Prop Firm EA Compliance: Four Infrastructure Gaps That Cause Challenge Disqualification

Prop Firm EA Compliance: Four Infrastructure Gaps That Cause Challenge Disqualification

23 July 2026, 21:27
Boris Armenteros
0
25

A client asked me to diagnose why his FTMO Phase 1 EA was disqualified on day 9. The strategy was profitable — positive P&L at the point of disqualification. The journal told the story: trades were opening after the daily loss limit had already been breached. The check was in OnTimer() , set to fire every 15 minutes. In the 14-minute window after the breach, three more trades executed. Compliance failure, not strategy failure.

This is the most common prop firm EA failure pattern I see: the strategy is sound, but the compliance layer either doesn't exist or has timing gaps. The trading logic has no awareness of account state — it only knows price moved and a signal fired. Four specific gaps cause the bulk of challenge disqualifications.

Daily Loss Enforcement: OnTick, Not OnTimer

The correct implementation checks total daily P&L on every OnTick()  call. Total daily P&L = all floating unrealized P&L plus all closed-trade results since session open, compared against startOfDayBalance × maxDailyLossPercent .

void OnTick()
{
   // Capture session start balance once per day
   static datetime lastSessionDate = 0;
   datetime currentDate = iTime(Symbol(), PERIOD_D1, 0);

   if(currentDate != lastSessionDate)
   {
      startOfDayBalance = AccountInfoDouble(ACCOUNT_BALANCE);
      dailyHaltFlag     = false;
      lastSessionDate   = currentDate;
   }

   // Check on every tick — not on a timer
   if(!dailyHaltFlag)
   {
      double dailyPnL = AccountInfoDouble(ACCOUNT_EQUITY) - startOfDayBalance;

      if(dailyPnL <= -(startOfDayBalance * IN_MaxDailyLossPercent / 100.0))
      {
         CloseAllPositions();
         DeleteAllPendingOrders();
         dailyHaltFlag = true;
         printf("%s: Daily loss limit reached. Trading halted.", __FUNCTION__);
      }
   }

   if(dailyHaltFlag) return;

   RunStrategyLogic();
}

The session-start balance capture matters. If you capture it once at EA initialization, subsequent days calculate against the wrong reference. After a profitable Monday, Tuesday's daily loss limit calculation starts from the wrong point — either allowing more room than it should or triggering too early. Capture it at the first tick of each session using broker server time, not local machine time or UTC.

Trailing Drawdown: Track Peak Equity

Prop firms don't enforce a fixed floor — the allowed drawdown follows your equity peak. An EA monitoring startBalance × 0.90  as a fixed floor gets caught every time the account makes progress.

On a $100,000 account with a 10% trailing limit:

  • Account opens: floor is $90,000
  • Equity peaks at $103,500: floor moves to $93,150
  • Equity peaks at $107,000: floor moves to $96,300

An EA tracking the fixed $90,000 floor believes it has $3,150 more room than it does after the first equity peak.

void UpdateTrailingDrawdown()
{
   double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);

   if(currentEquity > peakEquity)
      peakEquity = currentEquity;

   double trailingFloor = peakEquity * (1.0 - IN_MaxDrawdownPercent / 100.0);

   if(currentEquity <= trailingFloor && !drawdownHaltFlag)
   {
      CloseAllPositions();
      drawdownHaltFlag = true;
      printf("%s: Trailing floor hit. Peak=%.2f Floor=%.2f Equity=%.2f",
             __FUNCTION__, peakEquity, trailingFloor, currentEquity);
   }
}

Initialize peakEquity  in OnInit()  to the current account balance — not zero. An EA initializing peakEquity = 0  won't correctly track a floor until it first hits a new equity high.

Phase-Aware Position Sizing

Different challenge phases have different risk limits. A 2% risk-per-trade that fits a funded account's relaxed drawdown limits can breach Phase 1's daily loss limit after two consecutive losses. Building phase awareness into the lot size calculation:

enum ENUM_CHALLENGE_PHASE
{
   PHASE_CHALLENGE    = 0,  // Conservative — tightest limits
   PHASE_VERIFICATION = 1,  // Standard
   PHASE_FUNDED       = 2   // Relaxed limits
};

input ENUM_CHALLENGE_PHASE IN_Phase       = PHASE_CHALLENGE;
input double               IN_BaseRiskPct = 1.0;

double GetEffectiveRiskPercent()
{
   switch(IN_Phase)
   {
      case PHASE_CHALLENGE:    return IN_BaseRiskPct * 0.5;
      case PHASE_VERIFICATION: return IN_BaseRiskPct * 0.75;
      case PHASE_FUNDED:       return IN_BaseRiskPct;
   }
   return IN_BaseRiskPct * 0.5;  // Safe default
}

Log the active phase in OnInit() . When reviewing a journal, you want to see what configuration was running — not reconstruct it from memory weeks later.

MT5 Netting Mode Guard

Several prop firms have migrated to MT5 accounts in netting mode. An EA built for hedging mode compiles without error on a netting account and behaves incorrectly from the first trade.

On a hedging account, opening a long and then a short gives you two independent positions. On a netting account, the short partially or fully closes the long — there is no separate short position. An EA expecting a hedge now has no position. PositionsTotal()  returns 0, the strategy sees no open position, and re-enters — creating an unintended trade loop. No compile-time warning. No runtime error. Silent architectural mismatch.

int OnInit()
{
   printf("%s: Version %s | Phase=%d | MaxDailyLoss=%.1f%%",
          __FUNCTION__, VERSION, IN_Phase, IN_MaxDailyLossPercent);

   if(AccountInfoInteger(ACCOUNT_MARGIN_MODE) == ACCOUNT_MARGIN_MODE_RETAIL_NETTING)
   {
      printf("%s: WARNING - Netting account detected. EA requires hedging mode.", __FUNCTION__);
      return INIT_FAILED;
   }

   peakEquity = AccountInfoDouble(ACCOUNT_BALANCE);
   printf("%s: Initialized. PeakEquity=%.2f", __FUNCTION__, peakEquity);
   return INIT_SUCCEEDED;
}

The ACCOUNT_MARGIN_MODE_RETAIL_NETTING  constant is documented in the MQL5 reference under account property enumerations.

Architecture: Compliance Separate from Strategy

These four checks belong in a compliance module that is architecturally separate from the trading logic. No knowledge of the strategy's signal generation, no dependency on strategy variables, no compliance checks interleaved inside strategy functions.

The practical reason: when you update the strategy — new entry logic, revised exit conditions — you don't want to accidentally break the compliance checks. A standalone compliance module can be tested in isolation. Feed it a synthetic equity sequence and verify it fires exactly at the limit: not one trade early, not one trade late.

The trading strategy runs inside the compliance wrapper. RunStrategyLogic()  is only called after all compliance checks pass. That's the structural goal.

The mistakes above are preventable. They require building the compliance layer as a first-class engineering deliverable, not an afterthought layered onto a working strategy.