MT5 Lab

How to Recover MT5 EA State After Restart or Reconnect

Design a restart-safe MT5 EA that reconciles positions, pending orders, and trade history before it can send another signal after reconnect.

한국어 원문 읽기

Key takeaways

After a restart, rebuild EA state from server-side positions, pending orders, and history instead of trusting reset process memory.

Persist completed-bar and in-flight markers separately so an ambiguous pre-crash request is never resent automatically.

Block new requests until reconciliation succeeds, and enter a locked state when exposure, request outcome, or runtime ownership cannot be explained.

Recover MT5 EA state from account facts, not old memory

Key takeaways

  • When an EA reloads, process variables such as lastBarTime = 0 reset. 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 sourceExamplesRecovery treatment
Process memorylastBarTime, requestPending, local arraysRebuild; never treat as durable evidence
Current accountOpen positions, pending orders, SL/TPQuery again as current server facts
Trade historyOrders, deals, position identifiersUse to resolve an ambiguous in-flight request
Durable markerCompleted bar, in-flight bar, runtime ownerScope 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 stateMeaningNew requests
RECOVERINGChecking connection, permissions, exposure, orders, and markersBlocked
READYEvery declared invariant holds and no ambiguous request remainsAllowed only at a signal boundary
LOCKEDDuplicate runtime, unknown exposure, or unresolved in-flight request foundBlocked 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 boundary

TerminalInfoInteger(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.

MarkerMeaningBehaviour after restart
last_processed_barSignal boundary fully reconciledNever evaluate that bar again
inflight_barRequest started but final outcome not committedNever 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 windowPossible evidenceRecovery policy
Before in-flight writeNo request startedA later normal evaluation is possible
After in-flight write, before sendMarker exists but no server requestLock rather than guess and resend
After send, before responseOrder, deal, or position may existReconcile current state and history
After server completion, before completion markerFill exists while only in-flight remainsMatch deal and position identifier
After completion marker and in-flight clearProcessing committedDo 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.

  1. Block new entries on the current runtime.
  2. Record positions, pending orders, last completed bar, and in-flight state.
  3. Confirm from logs that the old EA is actually stopped.
  4. Start the target VPS or terminal in RECOVERING.
  5. Match account, server, symbol, timeframe, Inputs, and Magic Number.
  6. Enter READY only 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.

CheckREADY conditionLOCKED example
Connection and permissionsServer connected; terminal and account permit the EAFlapping connection or permission mismatch
Runtime ownershipExactly one active runtimeLocal and VPS heartbeats overlap
Current positionsCount, side, volume, and SL/TP fit the strategyUnknown position or missing required stop
Pending ordersTypes and count fit the strategyPending order exists for a market-only EA
Completed barKey scope matches and timestamp is not in the futureMarker reused across server or timeframe
In-flight requestNone, or outcome resolved from historyRequest outcome cannot be found
Trade historyOrders, deals, and identifier form one explanationVolume 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 test

If 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.

TestInjection pointPassing evidence
Reattach EAMid-bar with no positionCurrent bar is consumed; EA waits for the next bar
Restart with positionAfter server-side protection is visibleSame ticket/identifier recognized; no duplicate entry
ReconnectFirst tick after quote outageRECOVERING logged; request delayed beyond recovery tick
Ambiguous requestStop after in-flight persistenceNo automatic resend; account and history reconciled
Pending order remainsRestart after order acceptanceOrder found separately from positions
Local-to-VPS moveStop source, then start targetNo overlapping active interval; ownership transfer logged
Accidental double attachPlace the EA on two chartsSecond 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.