Let me tell you about the time I watched a "perfectly backtested" grid EA blow up an account in the first hour of live trading.
The backtest was beautiful. Profit factor: 2.3. Sharpe ratio: 1.8. Max drawdown: 12%. The forward test on demo: flawless for two weeks.
Within 60 minutes of going live on a real VPS, it had opened three phantom positions that didn't exist on the broker's side, tried to close them with
OrderClose(), failed silently, and then compounded the error by opening additional positions based on its corrupted internal state.The EA's logic wasn't the problem. The plumbing was. The backtest environment had been too kind.
After 14 years of rescuing EAs, I've seen this pattern repeat with remarkable consistency. Let me walk you through the five structural failures I find most often—and how to fix them. These aren't theoretical. Every single one has blown up a real account.
Failure #1: The "Fire and Forget" OrderSend() Myth
The single most common mistake is treating
OrderSend() as if it's a function that always succeeds.``
mql4
// ❌ PROBLEM: No return value check, no error handling
OrderSend(Symbol(), OP_BUY, 0.10, Ask, 3, sl, tp);
gridLevel++; // Increments regardless of whether the order actually filled!
`
In the Strategy Tester, every order fills instantly. In live trading, requotes, margin failures, and trade context busy conditions cause silent failures. The EA's internal state diverges from reality.
The fix is not just checking the return value—it's verifying the order actually exists:
`mql4
// ✅ FIX: Check return value, verify existence, then update state
int ticket = OrderSend(Symbol(), OP_BUY, 0.10, Ask, 3, sl, tp);
if(ticket < 0) {
Print(StringFormat("%s: OrderSend failed | Error=%d", __FUNCTION__, GetLastError()));
return; // Don't update state
}
// Now verify the order was actually placed
if(!OrderSelect(ticket, SELECT_BY_TICKET)) {
Print("Order #", ticket, " not found despite OrderSend returning ticket!");
return;
}
gridLevel++; // Only increment after confirmed fill
`
Reference: The MQL4 documentation explicitly states that OrderSend() returns -1 if the trade request is rejected . GetLastError() must be called immediately after . Yet I still review code where this basic check is missing.
Failure #2: Error 130—The Invisible Killer
Error 130 ("Invalid stops") is the most misunderstood error in MQL4. It rarely means what you think it means.
I fixed a trailing stop EA where OrderModify() was rejected on every tick with error 130. The EA logged "trailing active" for hours while the stop loss never moved.
Here's what most developers miss: Error 130 doesn't just mean your SL/TP price is "wrong." It means the price violates the broker's MODE_STOPLEVEL constraint—and that constraint changes with market conditions.
`mql4
// ❌ PROBLEM: Hardcoded stop distance
double newSL = Bid - 20 Point;
OrderModify(ticket, price, newSL, tp, 0);
// ✅ FIX: Query MODE_STOPLEVEL and validate
int stopLevel = (int)MarketInfo(Symbol(), MODE_STOPLEVEL);
double minStopDistance = stopLevel Point;
double maxStopDistance = 1000 Point; // Broker-specific limit
double newSL = Bid - 20 Point;
// Validate against minimum distance
if(Bid - newSL < minStopDistance) {
newSL = Bid - minStopDistance;
Print("SL adjusted to minimum allowed: ", newSL);
}
// Validate against maximum distance
if(Bid - newSL > maxStopDistance) {
newSL = Bid - maxStopDistance;
Print("SL adjusted to maximum allowed: ", newSL);
}
// Also check MODE_FREEZELEVEL (pending orders only)
int freezeLevel = (int)MarketInfo(Symbol(), MODE_FREEZELEVEL);
if(freezeLevel > 0 && MathAbs(newSL - Ask) < freezeLevel * Point) {
Print("Price too close to freeze level. Waiting.");
return;
}
if(OrderModify(ticket, price, newSL, tp, 0)) {
Print("SL modified successfully. Effective SL: ", newSL);
} else {
Print(StringFormat("OrderModify failed | Error=%d | SL=%.5f",
GetLastError(), newSL));
}
`
A Unique Perspective: Most developers treat MODE_STOPLEVEL as a static value they check once in OnInit(). This is wrong. Some brokers change the stop level during high volatility or news events. I once worked with a broker that set MODE_STOPLEVEL to 0 during normal conditions but increased it to 50 during major news releases. The EA that queried it on every tick survived; the one that cached it at startup got slaughtered.
Failure #3: The
OrderSelect() State Corruption Trap
Here's a subtle bug that took me a week to track down.
You're in a loop iterating through open orders. You select an order, get its ticket, then call OrderModify(). Then you select another order. But sometimes, the order you just modified disappears from the trade pool because it got filled or closed. When you try to select it again, OrderSelect() fails, but you don't check the return value.
`mql4
// ❌ PROBLEM: OrderSelect() failure goes unchecked
for(int i = OrdersTotal() - 1; i >= 0; i--) {
OrderSelect(i, SELECT_BY_POS);
int ticket = OrderTicket();
// ... do something with ticket ...
}
// ✅ FIX: Always check OrderSelect() return value
for(int i = OrdersTotal() - 1; i >= 0; i--) {
if(!OrderSelect(i, SELECT_BY_POS)) {
Print("OrderSelect failed at index ", i, " | Error: ", GetLastError());
continue; // Don't assume the order exists
}
int ticket = OrderTicket();
// ... do something with ticket ...
}
`
But here's the kicker—this becomes critical when combined with position modifications. I was working on a hedging EA that tracked order tickets in an array. After modifying a stop loss, the EA assumed the order's ticket remained valid. Then a partial fill occurred (the order executed on one side but not the other), and the ticket was invalidated mid-loop.
`mql4
// ✅ FIX: Track orders by ticket AND verify before each operation
int modifiedTickets[]; // Store tickets of modified orders
void TrackAndModifyOrders() {
ArrayResize(modifiedTickets, 0);
for(int i = OrdersTotal() - 1; i >= 0; i--) {
if(!OrderSelect(i, SELECT_BY_POS)) continue;
int ticket = OrderTicket();
// Check if this order has already been modified
bool alreadyModified = false;
for(int j = 0; j < ArraySize(modifiedTickets); j++) {
if(modifiedTickets[j] == ticket) {
alreadyModified = true;
break;
}
}
if(alreadyModified) continue; // Skip already modified orders
// Attempt modification
if(OrderModify(ticket, ...)) {
// Store ticket to prevent multiple modifications
int size = ArraySize(modifiedTickets);
ArrayResize(modifiedTickets, size + 1);
modifiedTickets[size] = ticket;
}
}
}
`
Failure #4: The
OrderModify() Double-Click Problem
Here's another subtle one: OrderModify() fails if the new SL/TP prices are identical to the current ones. Not invalid—identical.
`mql4
// ❌ PROBLEM: Modifying to the same value triggers error 1 (no error)
double currentSL = OrderStopLoss();
double newSL = currentSL; // Same as current
OrderModify(ticket, OrderOpenPrice(), newSL, tp, 0);
// This fails silently because there's no change to make
`
The fix is trivial but widely overlooked:
`mql4
// ✅ FIX: Check if modification is actually needed
double currentSL = OrderStopLoss();
double currentTP = OrderTakeProfit();
if(MathAbs(newSL - currentSL) < Point && MathAbs(newTP - currentTP) < Point) {
// No change needed—skip the OrderModify call entirely
Print("SL/TP unchanged, skipping modification.");
return;
}
OrderModify(ticket, OrderOpenPrice(), newSL, newTP, 0);
`
Failure #5: State Loss on Terminal Restart
Here's the biggest structural failure I see. Most EAs store all state in runtime variables—variables that reset to zero every time the terminal restarts.
A martingale EA I fixed tracked its multiplier in a static int variable. On Friday night, it had a losing chain active. Over the weekend, the VPS rebooted. On Monday morning, the EA restarted with the multiplier reset to 1—while the losing positions were still open. It opened a base lot position, the market moved against it, and the account was blown before anyone noticed.
The solution is two-tiered: persist critical state AND reconcile on startup.
`mql4
// ✅ FIX: Persist state to file AND reconcile with broker on startup
string stateFile = "ea_state_" + IntegerToString(Magic) + ".bin";
void SaveState() {
int handle = FileOpen(stateFile, FILE_WRITE|FILE_BIN);
if(handle != INVALID_HANDLE) {
FileWriteInteger(handle, gridLevel);
FileWriteInteger(handle, cycleCount);
FileWriteDouble(handle, averageEntry);
FileClose(handle);
}
}
void LoadAndReconcileState() {
int handle = FileOpen(stateFile, FILE_READ|FILE_BIN);
if(handle != INVALID_HANDLE) {
gridLevel = FileReadInteger(handle);
cycleCount = FileReadInteger(handle);
averageEntry = FileReadDouble(handle);
FileClose(handle);
}
// Now RECONCILE with actual broker state
// Count open positions with this Magic number
int actualPositions = 0;
for(int i = OrdersTotal() - 1; i >= 0; i--) {
if(OrderSelect(i, SELECT_BY_POS)) {
if(OrderMagicNumber() == Magic && OrderSymbol() == Symbol()) {
actualPositions++;
}
}
}
// If our internal state doesn't match reality, correct it
if(actualPositions != gridLevel) {
Print("State mismatch: internal=", gridLevel, " actual=", actualPositions);
gridLevel = actualPositions; // Broker is always the source of truth
}
}
int OnInit() {
LoadAndReconcileState();
return INIT_SUCCEEDED;
}
`
Reference: The MQL4 documentation confirms that expert advisors run in a separate thread and state is not preserved across terminal restarts . Reliance on static variables for critical state is a known anti-pattern .
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:
Does it check return values on every OrderSend, OrderModify, and OrderClose?
Does it validate SL/TP against MODE_STOPLEVEL on every modification attempt?
Does it check OrderSelect() return values in every loop?
Does it verify that modifications actually change values before calling OrderModify()`?If any answer is "no," the backtest results are fiction with a delayed fuse.
本文首发于FXEAR.com,原创内容,未经授权禁止转载。