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 type | Questions to answer from your firm's rulebook | How the guard handles it |
|---|---|---|
| Daily loss limit | Percentage 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 limit | Static 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 caps | Maximum positions, lots or trades per day? | A count of the EA's entries since the reset |
| News and weekend rules | Windows around releases? Holding over the weekend allowed? | A time filter in your entry logic |
| EA-specific rules | Are 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:
- If a new trading day has started (at your firm's reset hour), record the day's reference value.
- Compare current equity with the daily floor and the overall floor.
- If either floor is reached, close the EA's positions and stop trading until the next day.
- 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
- 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.
- Check that trading resumes after the reset hour.
- On a demo account, remove the EA mid-day and attach it again: the halt flag and the day's base should survive.
- 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:
- MQL5 Reference: AccountInfoDouble() — reading balance and equity.
- MQL5 Reference: GlobalVariableSet() — terminal global variables, which keep values across restarts.
- MQL5 Reference: HistorySelect() — loading the deal history for the day.
- MQL5 Reference: CTrade class — closing positions and checking the result.
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.