Your daily loss limit resets when MetaTrader restarts. Here is the fix.
The bug everyone ships
Almost every "daily loss guard" Expert Advisor keeps its state in memory:
double today_pnl = 0.0; // module scope
bool daily_blocked = false;
It works beautifully — until the terminal is restarted, the EA is recompiled, the chart is switched, or the profile is reloaded. Every one of those events calls OnInit() again with zeroed variables. Your "hard daily limit" politely hands you back the full day's risk allowance.
If you trade a prop-firm account with a 5% daily drawdown rule, that is not a small bug: the guard you are relying on to keep the account alive silently disarms itself exactly when things are going badly. Crashes and restarts cluster around bad sessions, not good ones.
What "persisted" has to mean
To be honest, the state has to survive four different events:
| Event | In-memory | Terminal global variables | File (common folder) |
|---|---|---|---|
| New tick, new bar | yes | yes | yes |
| EA recompiled / reattached | no | yes | yes |
| Terminal restart | no | yes | yes |
| Terminal reinstalled / data dir changed / another terminal | no | no | yes |
GlobalVariableSet() is enough for the common case and it is the cheapest option — global variables live in the terminal's data folder and survive a restart. Files win on the last row, which matters if you move machines or keep several terminals. The reference implementation below uses a file; the pattern is the same either way.
There is a fourth level that people forget: the account itself. Open positions and closed deals are already persisted by the broker. That does not replace your own file, but it lets you rebuild the day's counters from history when the file is missing or stale — which is exactly what you want after an unclean shutdown.
The three things to persist
struct GuardState
{
long day_stamp; // WHICH trading day this state belongs to
double day_start_balance; // the anchor for the day's drawdown
bool blocked; // the cap has been hit today
};
That is the minimum. day_stamp is the field people forget, and without it the restored state is either ignored or — worse — applied forever. A guard that restores yesterday's "blocked" flag and never clears it stops trading for good, and you will not notice until the account has been flat for a week.
Pitfall 1: use the broker's day, not the local clock
long TradingDayStamp()
{
MqlDateTime t;
TimeToStruct(TimeTradeServer(), t); // NOT TimeLocal(), NOT TimeCurrent() on historical data
return((long)t.year * 10000 + t.mon * 100 + t.day);
}
TimeLocal() resets your cap in the middle of the broker's session, and at the wrong moment around weekends. TimeCurrent() is the last known server tick time and can lag when the market is quiet. TimeTradeServer() is the broker clock, which is the clock the prop firm uses when it decides whether you broke the rule.
If you want to be strict, ask the broker which day it is rather than computing it: compare the server timestamp with your own day stamp and treat any rollover as a new day.
Pitfall 2: write state after the decision, never before
The dangerous ordering is:
// WRONG: a crash between these two lines lets one order through
blocked = true;
SaveState();
if(risk_ok) SendOrder();
Persist, then act — and re-read the flag immediately before the send if your EA can be interrupted:
if(blocked) return; // cheap, and catches an external trip
if(!risk_ok) { blocked = true; SaveState(); return; }
SendOrder();
The rule is simple: the persisted flag must never be less strict than the in-memory flag. When in doubt, save the stricter state and let the next run decide.
Pitfall 3: you cannot block other EAs from inside an EA
This is the one that surprises people. An Expert Advisor cannot stop the order another EA (or a Python strategy on the same account) is about to send — MQL5 gives no hook into another program's order path. Your options are:
1. Close positions when the cap trips. Defensive, and it is what most commercial guards do. 2. Publish a flag your own code respects. One line in the other EA:
if(GlobalVariableGet("GUARD_BLOCKED_" +
(string)AccountInfoInteger(ACCOUNT_LOGIN)) > 0.0) return;
It only works for EAs you control, but if all your strategies read it, the account is genuinely covered.
3. Enforce it outside the terminal. If the thing you need is "the terminal died at 03:40 and the position sat unprotected for two hours", no EA can help — the EA is dead too. That requires a separate process that watches the terminal and acts on it, which is a different kind of program entirely.
A compact, complete implementation
// ---- persistence ----
void SaveState()
{
int h = FileOpen(g_file, FILE_WRITE|FILE_TXT|FILE_COMMON|FILE_ANSI);
if(h == INVALID_HANDLE) { PrintFormat("state save failed: %d", GetLastError()); return; }
FileWriteString(h, StringFormat("{\"day\":%I64d,\"start\":%.2f,\"blocked\":%s}",
g_state.day_stamp, g_state.day_start_balance,
g_state.blocked ? "true" : "false"));
FileClose(h);
}
bool LoadState()
{
int h = FileOpen(g_file, FILE_READ|FILE_TXT|FILE_COMMON|FILE_ANSI);
if(h == INVALID_HANDLE) return(false); // no file yet: not an error
string s = "";
while(!FileIsEnding(h)) s += FileReadString(h);
FileClose(h);
if(StringLen(s) < 10) return(false);
g_state.day_stamp = (long)StringToInteger(Extract(s, "day"));
g_state.day_start_balance = StringToDouble(Extract(s, "start"));
g_state.blocked = (StringFind(s, "true") >= 0);
return(true);
}
void OnInit()
{
LoadState();
if(g_state.day_stamp != TradingDayStamp()) // new day: reset the anchor
{
g_state.day_stamp = TradingDayStamp();
g_state.day_start_balance = AccountInfoDouble(ACCOUNT_BALANCE);
g_state.blocked = false;
SaveState();
}
else if(g_state.blocked)
Print("Today's cap was already reached — still blocked.");
}
Note what happens on the else if branch: the EA does not silently resume trading. It tells you. A guard that recovers quietly is not a guard. Log lines are cheap; a drawdown you did not intend is not.
Rebuilding from the account when the file is missing
If LoadState() fails, do not assume the day is fresh. Rebuild from history instead:
double ClosedPnlToday()
{
long day = TradingDayStamp();
double pnl = 0.0;
HistorySelect(0, TimeCurrent());
int total = HistoryDealsTotal();
for(int i = 0; i < total; i++)
{
ulong ticket = HistoryDealGetTicket(i);
if(ticket == 0) continue;
datetime t = (datetime)HistoryDealGetInteger(ticket, DEAL_TIME);
MqlDateTime d; TimeToStruct(t, d);
long stamp = (long)d.year * 10000 + d.mon * 100 + d.day;
if(stamp != day) continue;
if(HistoryDealGetInteger(ticket, DEAL_ENTRY) != DEAL_ENTRY_OUT) continue;
pnl += HistoryDealGetDouble(ticket, DEAL_PROFIT)
+ HistoryDealGetDouble(ticket, DEAL_SWAP)
+ HistoryDealGetDouble(ticket, DEAL_COMMISSION);
}
return(pnl);
}
Use it as a cross-check: if the rebuilt number is worse than the stored one, trust the rebuilt number. The account is the only source of truth that cannot be deleted by accident.
A checklist before you go live
- The day stamp comes from the broker clock, not from the local machine. - The state file is written after every decision that can change it. - OnInit() either restores a state for today, or rebuilds one from history — never silently starts fresh. - When the cap trips, the EA says so in the journal, and the flag survives a restart. - You have tested it by killing the terminal, not by trusting the code. - If you run more than one strategy, they read a shared flag, and you know which strategies read it.
Testing it properly
The uncomfortable test, and the only one worth running:
1. Set a cap small enough to trip in a few minutes. 2. Let it trip. 3. Kill the terminal (not just the EA) with a position open. 4. Reopen it and try to take another trade.
If the new process happily sends the order, your state was in memory and the guard is theatre. I have watched this test fail on EAs that "support daily limits" — including paid ones. The test takes ten minutes and it is the difference between a risk limit and a wish.
Why not just use GlobalVariableSet()
Global variables are the cheapest option and they survive a terminal restart, so it is fair to ask why anyone bothers with a file. Three reasons:
1. Global variables live in the terminal's data folder, not in your account. Move to another machine, reinstall MetaTrader, or start a second portable copy, and your terminal is a different terminal with a different set of globals. 2. They are trivially wiped by the user. "Reset all globals" is one menu item away, and it is exactly the kind of thing a frustrated trader does after a bad session. 3. They are invisible. You cannot open a global in a text editor and see what the guard thought an hour ago; with a file you can, and that matters when you are trying to reconstruct why a trade was or was not taken.
The pragmatic answer: use globals if you only ever run one terminal on one machine, and mirror the same state into a file if you want to be able to prove anything later. The two are not exclusive, and the file costs one function call per decision.
Several terminals, several accounts
The moment you run more than one terminal, the state has to be keyed by the thing that distinguishes the accounts — the login. A file named `guard_state.json` will be shared by every terminal that can see the common folder, which means account A can read account B's day stamp. The fix is one line:
string g_file = StringFormat("guard_%I64d.json", AccountInfoInteger(ACCOUNT_LOGIN));
Do the same for the published block flag, so a strategy on account A does not stop trading because account B hit its cap. If you also run several strategies on the same account, add the strategy name to the file name and keep one shared flag for the account as a whole.
While you are there, decide what happens when two terminals write the same file at the same moment. `FILE_WRITE` truncates, so the loser of the race can leave a half-written file. Write to a temporary name and then rename, or accept the risk and rebuild from history on the next start — the account is your backstop either way.
What the tester does to your guard
Backtests are where guards go to lie to you. In the strategy tester the terminal is not restarted between runs, the common folder is not shared the way you expect, and every run starts with whatever state the previous run left behind. If your guard reads a file at the start of a run, run two backtests in a row and watch the second one behave differently.
Two habits make this harmless:
- Include a mode check and skip persistence in the tester unless you are deliberately testing the restore path: `if(MQLInfoInteger(MQL_TESTER)) { /* optional */ }`. - When you do test the restore path, test it in the tester on purpose, with a fixed state file, and assert the outcome in the log. A guard whose logic you only ever exercise in production is a guard you have never tested.
What to log so you can prove it worked
The cheapest insurance is a log line every time the guard changes its mind. Five fields are enough:
[guard] day=20261003 pnl=-48.20 cap=50.00 blocked=true reason=daily_cap action=no_new_trades restored_from=file
With that, a bad day is reconstructable in one sentence, and you can answer the only question that matters afterwards: did the guard stop trading because the cap was hit, or because something else broke? Without the line, you will guess, and you will guess in favour of the code you wrote.
Common objections
*"My EA already has a daily loss limit."* Ask where the number lives after a restart. If the answer is a variable, you have a limit that lasts until the next crash — which is exactly when you need it.
*"I never restart my terminal."* Then you have never had a Windows update, a VPS reboot, a broker disconnect that required a restart, or a crash. The guard exists for the day your habits fail.
*"I only trade one strategy, so the shared flag is overkill."* Then skip it. The first two layers still apply, and they are the ones that fail most often.
*"This is a lot of code for a safety net."* It is roughly forty lines, and most of them are the file read and write. The alternative is discovering the gap during the drawdown the guard was supposed to prevent.
*"What about a terminal-level solution?"* MetaTrader does not expose one. Risk limits that live only inside an EA inherit the EA's lifetime, which is the whole problem.
Putting it together: the smallest useful guard
If you want the shortest version that is still honest, it is this:
void OnTick()
{
if(g_blocked) return; // survived a restart
if(TradingDayStamp() != g_day) ResetDay(); // broker day, not local
if(DayLoss() <= -g_cap)
{
g_blocked = true; SaveState(); // persist AFTER the decision
PrintFormat("[guard] blocked day=%I64d pnl=%.2f", g_day, DayLoss());
return;
}
// ... your strategy here
}
Everything else in this article is detail around those six lines: where the state lives, which clock defines the day, and what you do when the file is missing. Get those three decisions right and the guard stops being decoration.
Where this leaves you
An in-terminal guard can enforce a daily loss cap and a per-trade risk budget, and it should. It cannot restart a dead terminal, watch five accounts from one screen, or review an order before your strategy sends it — those need to live outside MetaTrader. Knowing which failure modes belong to which layer is most of the work; the code above is the easy part.
Persist the state, use the broker's day, and write it after the decision. Those three lines of discipline are the difference between a risk limit and a wish.
Related notes
- What a kill switch should actually do (and the four ways they fail)
- Silent failures in trading automation: the three that cost the most
- Which day is it? Broker time, host time, and the trading-day boundary
Get the guard
The free, restart-proof MT5 guard EA (source included, no DLLs, no external calls): https://xuks124.github.io/vigildesk/free.html
Risk disclosure
Algorithmic trading carries both technical and market risk. No tool eliminates the possibility of loss. Nothing on this page is investment advice, and no performance is promised or implied.