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:
- Number of entries opened today
- Peak equity for an intraday drawdown stop
- A "locked for today" flag after a loss limit is hit
- 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;
}
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;
}
}
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)
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?
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.
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.
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.
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.
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.