Edgoro
Risk

MT5 EA prop firm rules: coding a daily loss limit and drawdown guard in MQL5

Make an MT5 Expert Advisor respect prop-firm-style rules: a daily loss guard that survives restarts, an overall loss floor and a trade cap, with MQL5 code.

An Expert Advisor trades when you're asleep, at lunch or staring at a different screen. On a funded-account challenge that's a problem: one bad sequence can break the daily loss limit before you notice, and the account is gone. If an EA runs on a challenge account, the rules have to live in its code.

This article shows how to build a risk guard in MQL5 that enforces the rule types most challenges use: a daily loss limit, an overall loss floor and a cap on trades. It describes rule types in general terms. Every firm defines them a little differently, and rules change, so read your firm's current rulebook and set the inputs from it.

Which rules the EA needs to know about

Rule typeQuestions to answer from your firm's rulebookHow the guard handles it
Daily loss limitPercentage of the starting balance, or of the day's balance or equity? Does floating loss count? When does the day reset, in server time?An equity floor recalculated at the reset hour
Overall loss limitStatic floor, or trailing a peak? Balance or equity? Does trailing stop at some level?A fixed floor, with a trailing variant below
Trade or lot capsMaximum positions, lots or trades per day?A count of the EA's entries since the reset
News and weekend rulesWindows around releases? Holding over the weekend allowed?A time filter in your entry logic
EA-specific rulesAre certain behaviours (tick scalping, copy trading, latency arbitrage) prohibited?Not something code can check: read the rules

For a plain-English tour of these rules, see prop firm challenge rules explained.

Design: a guard that runs before anything else

The guard is a gate that runs at the top of OnTick() and on a one-second timer:

  1. If a new trading day has started (at your firm's reset hour), record the day's reference value.
  2. Compare current equity with the daily floor and the overall floor.
  3. If either floor is reached, close the EA's positions and stop trading until the next day.
  4. Otherwise, let the strategy run. Before each new order, also check the daily trade cap.

The reference value is stored in a terminal global variable, not an ordinary variable, so a restart, a recompile or a dropped connection in the middle of the day doesn't reset it.

The code

#include <Trade/Trade.mqh>

enum ENUM_DAY_BASE
  {
   DAY_BASE_BALANCE,   // Balance at the day's start
   DAY_BASE_EQUITY,    // Equity at the day's start
   DAY_BASE_HIGHER     // Higher of the two
  };

input double        InpDailyLossPct   = 4.0;             // Daily loss limit, % (keep it below the firm's)
input bool          InpDailyFromStart = true;            // true: % of the starting balance; false: % of the day's base
input ENUM_DAY_BASE InpDayBase        = DAY_BASE_HIGHER; // The day's reference value
input double        InpMaxLossPct     = 8.0;             // Overall loss limit, % of the starting balance
input double        InpStartBalance   = 100000;          // Account size the rules refer to
input int           InpResetHour      = 0;               // Server hour at which the firm's day starts
input int           InpMaxTradesDay   = 3;               // Max new trades per day (0 = no cap)
input ulong         InpMagic          = 261105;          // Magic number of this EA

CTrade trade;
string gvBase, gvDay, gvHalt;

datetime DayStart(const datetime t)
  {
   long s = (long)t - InpResetHour * 3600;
   s -= s % 86400;
   return (datetime)(s + InpResetHour * 3600);
  }

double DayBaseValue()
  {
   double bal = AccountInfoDouble(ACCOUNT_BALANCE);
   double eq  = AccountInfoDouble(ACCOUNT_EQUITY);
   if(InpDayBase == DAY_BASE_BALANCE)
      return bal;
   if(InpDayBase == DAY_BASE_EQUITY)
      return eq;
   return MathMax(bal, eq);
  }

// Records the day's reference value once per trading day. Global variables survive restarts.
void UpdateDay()
  {
   double today = (double)DayStart(TimeCurrent());
   if(!GlobalVariableCheck(gvDay) || GlobalVariableGet(gvDay) != today)
     {
      GlobalVariableSet(gvDay, today);
      GlobalVariableSet(gvBase, DayBaseValue());
      GlobalVariableSet(gvHalt, 0.0);
     }
  }

void CloseOwnPositions()
  {
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || PositionGetInteger(POSITION_MAGIC) != (long)InpMagic)
         continue;
      if(!trade.PositionClose(ticket))
         PrintFormat("Close #%I64u failed: %u %s", ticket, trade.ResultRetcode(), trade.ResultRetcodeDescription());
     }
  }

// False once a floor has been reached: the EA's positions are closed and trading stops until the next day.
bool GuardOk()
  {
   UpdateDay();
   if(GlobalVariableGet(gvHalt) > 0.0)
      return false;

   double equity       = AccountInfoDouble(ACCOUNT_EQUITY);
   double base         = GlobalVariableGet(gvBase);
   double dailyLoss    = (InpDailyFromStart ? InpStartBalance : base) * InpDailyLossPct / 100.0;
   double dailyFloor   = base - dailyLoss;
   double overallFloor = InpStartBalance * (1.0 - InpMaxLossPct / 100.0);

   if(equity <= dailyFloor || equity <= overallFloor)
     {
      CloseOwnPositions();
      GlobalVariableSet(gvHalt, 1.0);
      PrintFormat("Guard: equity %.2f reached a floor (daily %.2f, overall %.2f). Trading halted.", equity, dailyFloor, overallFloor);
      return false;
     }
   return true;
  }

int TradesToday()
  {
   if(!HistorySelect(DayStart(TimeCurrent()), TimeCurrent()))
      return 0;
   int count = 0;
   for(int i = HistoryDealsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = HistoryDealGetTicket(i);
      if(ticket == 0)
         continue;
      if(HistoryDealGetInteger(ticket, DEAL_MAGIC) == (long)InpMagic && HistoryDealGetInteger(ticket, DEAL_ENTRY) == DEAL_ENTRY_IN)
         count++;
     }
   return count;
  }

bool CanOpenNewTrade()
  {
   if(!GuardOk())
      return false;
   return InpMaxTradesDay <= 0 || TradesToday() < InpMaxTradesDay;
  }

int OnInit()
  {
   string prefix = "guard_" + IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)) + "_" + IntegerToString((long)InpMagic);
   gvBase = prefix + "_base";
   gvDay  = prefix + "_day";
   gvHalt = prefix + "_halt";
   trade.SetExpertMagicNumber(InpMagic);
   EventSetTimer(1);   // check the floors even when this symbol is quiet
   return INIT_SUCCEEDED;
  }

void OnDeinit(const int reason)
  {
   EventKillTimer();
  }

void OnTimer()
  {
   GuardOk();
  }

void OnTick()
  {
   if(!GuardOk())
      return;
   // Your strategy logic goes here. Before sending any new order:
   // if(CanOpenNewTrade()) { size the trade from risk and send it with a stop-loss }
  }

How it works

The trading day. DayStart() shifts the server clock so the firm's reset hour becomes midnight, cuts it to the day, and shifts it back. If your firm resets at 17:00 New York time and your broker's server runs seven hours ahead of New York, that's hour 0 on the server. Check both clocks; don't assume.

The reference value. Firms measure the daily limit from different things. InpDayBase picks balance, equity or the higher of the two at the reset, and InpDailyFromStart chooses whether the percentage applies to the starting account size or to that day's value. Between them they cover the common definitions.

Restarts. The day, the reference value and the halt flag live in terminal global variables named after the account and the magic number. Attach the EA again mid-day and it picks up where it left off. If you attach it for the first time mid-day, though, it records the current values as the day's base: start it before the reset, or set the first day's base by hand.

The timer. OnTick() only runs when this chart's symbol ticks. The one-second timer checks the floors anyway, which matters when the EA's losses come from another symbol or the market is quiet.

Own positions only. The guard closes positions that carry its magic number. Your firm's equity check includes everything on the account, including manual trades and other EAs, so keep those in mind or extend the loop.

Buffers: keep your limits below the firm's

By the time equity touches a floor, the guard still has to send close orders. Spread, slippage and gaps can push the final loss past the level, and the firm's server measures equity, not your EA. That's why the example uses 4% where a rulebook might say 5%: the gap is your margin for execution.

Per-trade risk matters as much as the floor. If a single trade can lose more than the daily buffer, the guard can't save you. Work backwards from the daily limit, as described in position sizing and the 1% rule, and turn the result into lots with a risk-based lot size function. The prop firm calculator does the arithmetic.

A trailing overall limit

Some firms trail the overall floor behind the highest balance or equity. A variant of the floor calculation, to use in GuardOk() in place of the static overallFloor:

// Trailing variant: the floor follows the highest equity the EA has seen.
// Pass lockAtStart = true if your firm stops trailing once the floor reaches the starting balance.
double TrailingFloor(const bool lockAtStart)
  {
   string gvPeak = gvBase + "_peak";
   double equity = AccountInfoDouble(ACCOUNT_EQUITY);
   double peak   = GlobalVariableCheck(gvPeak) ? GlobalVariableGet(gvPeak) : InpStartBalance;
   if(equity > peak)
     {
      peak = equity;
      GlobalVariableSet(gvPeak, peak);
     }
   double level = peak - InpStartBalance * InpMaxLossPct / 100.0;
   return lockAtStart ? MathMin(level, InpStartBalance) : level;
  }

The EA only sees equity when it runs, so a brief spike between checks can raise the firm's peak without raising yours. Another reason for a buffer.

Testing the guard

  1. In the Strategy Tester, set an absurdly low daily limit (0.3%, say) and check in the journal that positions close and new trades stop for the rest of the day.
  2. Check that trading resumes after the reset hour.
  3. On a demo account, remove the EA mid-day and attach it again: the halt flag and the day's base should survive.
  4. Compare the reset with your firm's dashboard for a few days.

Also run through the MQL5 Expert Advisor risk checklist: stops on every order, spread filter, once-per-bar entries.

Sources and further reading

Your firm's rulebook is the authority on its own limits. For the MQL5 functions the guard uses:

Try it with Edgoro

Tell Edgoro your challenge's rules in plain English — "daily loss 4% of the starting balance, reset at 00:00 server time, overall floor 8%, max 3 trades a day, risk 0.5% per trade" — together with your strategy, and it writes an MT5 Expert Advisor with the guard built in. Test it on a demo or trial account first. Build yours with Edgoro.

This code is an educational example; it doesn't guarantee compliance with any firm's rules. Read your firm's rulebook, test on a demo account, and remember that trading involves substantial risk of loss.

Build it with Edgoro

Describe your strategy in plain English and get a TradingView indicator in Pine Script v6 or a MetaTrader 5 Expert Advisor in minutes. Every first purchase is covered by a 7-day money-back guarantee.

Build your indicator with Edgoro