DEV Community

Omais Ali
Omais Ali

Posted on

Saving Expert Advisor State Across Restarts in MQL5

An MT5 terminal restart, a VPS reboot, a recompile, a timeframe switch or a changed input all reinitialize your Expert Advisor. Every global variable starts from zero again. If the EA was counting today's trades, tracking peak equity for a drawdown stop, or remembering that it already hit its daily loss limit, that memory is gone and the EA may happily start trading again.

This post covers three ways to keep state, and a simple rule for choosing between them.

Decide what actually needs to survive

Typical candidates:

  1. Number of entries opened today
  2. Peak equity for an intraday drawdown stop
  3. A "locked for today" flag after a loss limit is hit
  4. The last grid level or last entry price

Some of these can be rebuilt from the server. Some cannot. That difference drives the design.

Option 1: Rebuild from trade history

The trade server is the best source of truth. Positions and deals survive anything that happens on your machine. Counting today's entries is a history query, not a variable:

input long InpMagic = 240101;

datetime DayStart()
{
   MqlDateTime t;
   TimeToStruct(TimeCurrent(), t);
   t.hour = 0; t.min = 0; t.sec = 0;
   return StructToTime(t);
}

int EntriesToday()
{
   int count = 0;
   if(!HistorySelect(DayStart(), TimeCurrent()))
      return 0;
   int total = HistoryDealsTotal();
   for(int i = 0; i < total; i++)
   {
      ulong ticket = HistoryDealGetTicket(i);
      if(HistoryDealGetInteger(ticket, DEAL_MAGIC) != InpMagic) continue;
      if(HistoryDealGetString(ticket, DEAL_SYMBOL) != _Symbol) continue;
      if(HistoryDealGetInteger(ticket, DEAL_ENTRY) == DEAL_ENTRY_IN) count++;
   }
   return count;
}
Enter fullscreen mode Exit fullscreen mode

Note that DayStart uses server time. If your prop firm resets the day in another time zone, shift it accordingly.

Option 2: Terminal global variables

Values that cannot be derived, like peak equity, need storage. Terminal global variables are the quickest option. They survive restarts, but they only hold doubles and are deleted after four weeks without access.

Key them by magic number and symbol so two copies of the EA never share state:

double gPeakEquity  = 0.0;
bool   gLockedToday = false;

string Key(const string name)
{
   return "EA_" + IntegerToString(InpMagic) + "_" + _Symbol + "_" + name;
}

void SaveState()
{
   GlobalVariableSet(Key("day"), (double)DayStart());
   GlobalVariableSet(Key("peak"), gPeakEquity);
   GlobalVariableSet(Key("locked"), gLockedToday ? 1.0 : 0.0);
   GlobalVariablesFlush();
}

void LoadState()
{
   bool sameDay = GlobalVariableCheck(Key("day")) &&
                  (datetime)GlobalVariableGet(Key("day")) == DayStart();
   if(sameDay)
   {
      gPeakEquity  = GlobalVariableGet(Key("peak"));
      gLockedToday = GlobalVariableGet(Key("locked")) > 0.5;
   }
   else
   {
      gPeakEquity  = AccountInfoDouble(ACCOUNT_EQUITY);
      gLockedToday = false;
   }
}
Enter fullscreen mode Exit fullscreen mode

Call LoadState() in OnInit and SaveState() whenever a value changes. Do not rely on OnDeinit alone. A crash or power cut skips it entirely.

Option 3: A small file

For strings, arrays or anything structured, write a file in the MQL5/Files sandbox. Write to a temporary name first and then rename it with FileMove and the FILE_REWRITE flag, so a crash halfway through never leaves a corrupted state file behind.

The rule: when they disagree, trust the server

Rebuild everything you can from positions and history. Persist only what cannot be derived. On startup, if a stored value conflicts with what the server says, for example a stored open grid level with no matching position, believe the server and log the mismatch.

Test it like a restart

The Strategy Tester never restarts your EA mid run, so it will not catch these bugs. On a demo account, open a trade, remove the EA from the chart, attach it again and check the journal. Then do the same with a full terminal restart.

It is a ten minute test that prevents the classic "it opened five trades after the VPS rebooted" morning.

If you would rather have restart safe state built into your EA from the start, I build custom MT4 and MT5 Expert Advisors at Custom Trading Bots.

Top comments (6)

Collapse
 
stratcorealpha profile image
Arnold Holm •

The HistorySelect failure path returns 0, which is also a valid 'no entries today' result. Missing history after a restart could therefore reopen the daily allowance. I would return success separately from the count and block new entries until history is available. How would your restart test handle that failure?

Collapse
 
omaisali profile image
Omais Ali •

Good catch, Arnold, you are right. Returning 0 on failure makes "history not loaded" look exactly like "no trades today", and right after a restart is when history is most likely to be late. I would change it to return a bool for success and pass the count out by reference. If HistorySelect fails, the EA treats the daily allowance as used up, blocks new entries, logs the reason, and tries again on the next tick or timer event.

For the restart test I would close one trade earlier in the day, restart the terminal, and check that no new entry is allowed until the function succeeds and reports a count of 1. A second test starts the EA while the terminal is disconnected, so the history call fails, and confirms the EA stays flat instead of trading.

Collapse
 
stratcorealpha profile image
Arnold Holm •

That covers the failure case. I would add a successful empty-history test too: success=true with count=0 should leave the entry allowance available, while success=false with count=0 must block it. Keeping those two cases beside your closed-trade test checks both sides of the original ambiguity. I would force the failure result in the test rather than assume a disconnect always makes HistorySelect fail.

Thread Thread
 
omaisali profile image
Omais Ali •

Agreed, that pair is the real test. Success with a count of zero has to leave the entry open, failure with a count of zero has to block it, and checking only one of them leaves half of the original bug untested. Forcing the failure is a good call too. I usually wrap HistorySelect behind a small function the test can override to return false, because a real disconnect does not reliably make it fail, and a test that depends on unplugging the network will not run the same way twice.

Thread Thread
 
stratcorealpha profile image
Arnold Holm •

I would add one transition test to that wrapper: fail once, then return success with count=0 on the next call. The first result must block entry, and the second must restore the allowance without leaving a stale failure flag behind. That checks recovery as well as safe failure.

Thread Thread
 
omaisali profile image
Omais Ali •

Good addition. A failure flag that sticks after recovery is a nastier bug than the original one, because the EA looks safe and simply never trades again. I would also assert the order of events in that test: blocked on the first tick, allowed on the second, and no extra HistorySelect retries in between, so the recovery path cannot quietly turn into a loop.