Five Structural Failures Hiding in "Working" EAs

Five Structural Failures Hiding in "Working" EAs

27 June 2026, 19:24
Boris Armenteros
2
214

The same five structural gaps often hide inside a "working" EA. This is not about strategy logic. The strategies are often fine. The failures are in the plumbing: error handling, state management, environment assumptions, execution validation, and defensive guards.

Here are the five patterns a rescue review checks first, with MQL4 code examples showing both the problem and the fix.

1. No Error Handling on OrderSend

The EA sends a trade request and never checks what happened:

// Problem: no return value check
OrderSend(Symbol(), OP_BUY, 0.10, Ask, 3, sl, tp);
gridLevel++;  // Increments regardless of whether the order filled

In the Strategy Tester, every order fills. In live trading, requotes, margin failures, and trade context busy conditions cause silent failures. The EA's internal state diverges from reality.

// Fix: check return value, verify, then update state
int ticket = OrderSend(Symbol(), OP_BUY, 0.10, Ask, 3, sl, tp);
if(ticket < 0)
{
   printf("%s: OrderSend failed | Error=%d", __FUNCTION__, GetLastError());
   return;
}
gridLevel++;  // Only increment after confirmed fill

Consider a grid EA where the counter increments on every OrderSend() call regardless of the result. In live trading, requotes during volatile sessions create phantom grid levels. The average entry price in the EA's memory diverges from the account's actual average.

2. No State Persistence Across Restarts

The EA stores all state in runtime variables:

// Problem: static variable resets on terminal restart
static int multiplier = 1;

Terminal restarts constantly — VPS reboots, MetaTrader updates, broker disconnects. Every restart fires OnDeinit()  then OnInit() , and every static  variable resets to its initial value.

Consider a martingale EA that tracks its multiplier this way. After a weekend restart during a drawdown, the multiplier resets to 1. The EA opens a base lot position while the losing chain is still open.

// Fix: persist to file and reconcile on init
void SaveState()
{
   int handle = FileOpen("ea_state.bin", FILE_WRITE|FILE_BIN);
   if(handle != INVALID_HANDLE)
   {
      FileWriteInteger(handle, multiplier);
      FileWriteInteger(handle, cycleCount);
      FileClose(handle);
   }
}

int OnInit()
{
   // Reconcile: rebuild state from open orders + persisted data
   int handle = FileOpen("ea_state.bin", FILE_READ|FILE_BIN);
   if(handle != INVALID_HANDLE)
   {
      multiplier = FileReadInteger(handle);
      cycleCount = FileReadInteger(handle);
      FileClose(handle);
   }
   // Cross-check against actual open orders
   ReconcileWithBrokerState();
   return INIT_SUCCEEDED;
}

The global variable pool ( GlobalVariableSet , GlobalVariableGet ) also survives restarts and is simpler for single-value persistence.

3. Hardcoded Environment Assumptions

Point * 10  is correct for 5-digit forex pairs. It is wrong for gold, indices, and 4-digit brokers:

// Problem: hardcoded pip calculation
double pipSize = Point * 10;  // Wrong on XAUUSD, wrong on 4-digit brokers
double minLot = 0.01;         // Fails on brokers with 0.10 minimum
// Fix: query symbol properties at runtime
double pipSize = Point * MathPow(10, Digits % 2);
double minLot = MarketInfo(Symbol(), MODE_MINLOT);
double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP);
int stopLevel = (int)MarketInfo(Symbol(), MODE_STOPLEVEL);

Every hardcoded value in the first block can break an EA. The runtime queries in the second block prevent it.

4. Ignoring Execution Feedback

Beyond OrderSend() , an EA must never ignore OrderModify() , OrderClose() , or OrderDelete()  return values:

// Problem: fire and forget
OrderModify(ticket, price, newSL, tp, 0);
printf("SL updated to %.5f", newSL);  // Logs intent, not result

I fixed a trailing stop EA where OrderModify()  was rejected every tick with error 130 (invalid stops) because the new SL violated MODE_STOPLEVEL . The EA logged "trailing active" while the stop loss had not moved in hours.

// Fix: pre-validate and log effective values
double stopsLevel = MarketInfo(Symbol(), MODE_STOPLEVEL) * Point;
double effectiveSL = MathMin(newSL, Bid - stopsLevel);

if(effectiveSL > currentSL + minDelta)
{
   if(OrderModify(ticket, price, effectiveSL, tp, 0))
   {
      printf("%s: SL modified | Effective=%.5f Requested=%.5f",
             __FUNCTION__, effectiveSL, newSL);
   }
   else
   {
      printf("%s: OrderModify failed | Error=%d", __FUNCTION__, GetLastError());
   }
}

5. No Defensive Guards

The final pattern: no bounds checking, no zero-denominator guards, no validation of indicator return values.

// Problem: ATR can return zero on thin markets
double sl = iATR(Symbol(), 0, 14, 0);
double lots = riskAmount / (sl / Point);  // Division by zero when ATR = 0

Consider the same calculation during a news spike: ATR collapses to effectively zero on a single bar, and the division returns an enormous lot size. NormalizeDouble() rounds it without complaint, and OrderSend() submits it. What stops that order is outside the EA: the symbol's maximum lot size and the broker's margin check.

// Fix: guard every denominator and array access
if(Bars < requiredBars)
{
   printf("%s: Insufficient history | Bars=%d Required=%d",
          __FUNCTION__, Bars, requiredBars);
   return;
}

double sl = iATR(Symbol(), 0, 14, 0);
if(sl <= 0)
{
   printf("%s: ATR returned zero | Skipping", __FUNCTION__);
   return;
}

double lots = NormalizeDouble(riskAmount / (sl / Point), 2);
lots = MathMax(lots, MarketInfo(Symbol(), MODE_MINLOT));
lots = MathMin(lots, MarketInfo(Symbol(), MODE_MAXLOT));

The Pattern

All five failures share the same root cause: the EA was tested only in the Strategy Tester — the environment where everything works. Orders fill instantly. Terminals never restart. Symbol properties match the developer's setup. History is complete.

Live trading is the environment where things fail. Production-ready code assumes that every trade request can be rejected, every stored value can be lost, and every assumption about the broker can be wrong.

Five questions to check any EA:

  1. Does it check return values on every OrderSend , OrderModify , and OrderClose ?
  2. Does it persist critical state and reconcile on startup?
  3. Does it query symbol properties at runtime?
  4. Does it validate execution feedback and log effective values?
  5. Does it guard against zero denominators and insufficient history?

If any answer is "no," the backtest results are fiction with a delayed fuse.

Correction, 9 October 2026: an earlier version said this scenario produced an infinite lot size that NormalizeDouble() turned into a very large number, and that the broker's lot limit was the only thing that stopped the order. The paragraph now describes a near-zero ATR producing an enormous lot size, and names what stops such an order outside the EA: the symbol's maximum lot size and the broker's margin check.