Your EA's drawdown guard probably counts a withdrawal as a loss

30 August 2026, 04:37
Ihsan Ullah
0
28

Last week a live account running my gold EA closed every open position and locked itself out for the rest of the day. There was no losing streak, no spike, no news event. The owner had simply withdrawn part of the balance.

If your EA has a daily loss limit or a max-drawdown guard, it very probably has this bug too. It is worth ten minutes to check.

Why it happens

Almost every risk guard is written the same way: store a baseline at the start of the day, then compare live equity against it.

double dayPnL = AccountInfoDouble(ACCOUNT_EQUITY) - g_dayStartEquity;
if(DailyLossLimit > 0 && dayPnL <= -DailyLossLimit)
   CloseAll();

double dd = (g_peakEquity - eq) / g_peakEquity * 100.0;
if(dd >= MaxDrawdownPct)
   { CloseAll(); PauseForTheDay(); }

That is correct for trading P&L. It is wrong for cash movement. A withdrawal lowers ACCOUNT_EQUITY without a single trade going against you, so dayPnL swings sharply negative and the drawdown percentage jumps. Both guards fire. The EA closes live positions that were doing nothing wrong, and then refuses to trade.

A deposit produces the mirror image: your daily profit target is satisfied by money you just added.

The fix

Detect balance operations and shift every baseline by the same amount. MT5 books deposits and withdrawals as deals of type DEAL_TYPE_BALANCE, so they are straightforward to find:

double ScanDayBalanceOps()
{
   MqlDateTime d; TimeToStruct(TimeCurrent(), d);
   d.hour = 0; d.min = 0; d.sec = 0;
   if(!HistorySelect(StructToTime(d), TimeCurrent() + 86400)) return 0.0;

   double sum = 0.0;
   int    n   = HistoryDealsTotal();
   for(int i = 0; i < n; i++)
   {
      ulong tk = HistoryDealGetTicket(i);
      if(tk == 0) continue;
      long type = (long)HistoryDealGetInteger(tk, DEAL_TYPE);
      if(type == DEAL_TYPE_BALANCE || type == DEAL_TYPE_CREDIT)
         sum += HistoryDealGetDouble(tk, DEAL_PROFIT);
   }
   return sum;
}

Then, before the gates read equity:

void SyncBalanceOperations()
{
   double bal = AccountInfoDouble(ACCOUNT_BALANCE);
   if(MathAbs(bal - g_lastSeenBal) < 0.005) return;   // balance did not move
   g_lastSeenBal = bal;

   double now   = ScanDayBalanceOps();
   double shift = now - g_balOpsApplied;
   if(MathAbs(shift) < 0.005) return;                 // ordinary trade P&L
   g_balOpsApplied = now;

   g_dayStartEquity  += shift;
   g_peakEquity      += shift;
   g_dayStartBalance += shift;
   g_initialBalance  += shift;
}

Shift the baselines and the measured P&L is unchanged by the transfer, which is the correct behaviour.

Three details that matter

Only scan when the balance actually moves. Selecting history on every tick is wasteful. Balance changes on a trade close or on a transfer, so use that as the trigger and skip the scan otherwise.

Scope the scan to today. HistorySelect across the full account history gets slow on a long-lived account. Anything older than the current day is already baked into the baseline by definition.

Skip it in the tester. There are no cash transfers in a backtest, and a per-close history scan across thousands of deals will slow an optimisation to a crawl.

What should NOT be adjusted

Position sizing. If risk is a percentage of balance and half the balance leaves the account, the lots you can afford should shrink. That is correct behaviour, not a bug. Only the risk baselines need the shift.

The one that catches people out

If your EA persists day state to global variables and restores it on load, a transfer made while the EA was switched off will restore a stale, pre-transfer baseline. Reconcile in OnInit as well as on tick, otherwise the first thing your freshly fixed EA does is trip on the old figure.

Worth adding to your test checklist: attach the EA to a demo account, make a small withdrawal, and confirm the day P&L does not move. It is a two-minute test that most of us have never run.

I build and document risk-first MT5 systems at goldscalpers.com.