Why your MT5 EA works in the Strategy Tester but not on a live chart
Most EAs that die on a live chart fail for unglamorous reasons: an order rejected because the stop is inside the broker's minimum distance, a filling mode the server refuses, or code that assumes a position exists the instant OrderSend returns. The Strategy Tester hides all of this. It preloads history, always permits trading, ignores stops levels, and executes orders synchronously. Strip those assumptions away and the same binary behaves very differently.
Symptom
The EA compiles, runs in the tester, opens trades. Attach it to a live chart and one of three things happens: nothing at all, one trade and then silence, or a stream of OrderSend failed entries in the Experts tab that you never saw during testing.
1. Trading permissions
In the tester, TERMINAL_TRADE_ALLOWED and ACCOUNT_TRADE_EXPERT are always true. Live, they depend on the AutoTrading button and your account settings. Check before every entry, not just in OnInit.
bool CanTrade(const string symbol)
{
if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
{ Print("AutoTrading is off"); return false; }
if(!AccountInfoInteger(ACCOUNT_TRADE_EXPERT))
{ Print("Algo trading disabled on account"); return false; }
if(!MQLInfoInteger(MQL_TRADE_ALLOWED))
{ Print("EA not permitted to trade"); return false; }
if(SymbolInfoInteger(symbol, SYMBOL_TRADE_MODE) != SYMBOL_TRADE_MODE_FULL)
{ Print("Symbol is close-only"); return false; }
return true;
}
2. Filling mode
The tester accepts whatever you send. Real servers often do not, and you get retcode 10030 (Unsupported filling mode). Ask the symbol what it supports.
ENUM_ORDER_TYPE_FILLING PickFilling(const string symbol)
{
long modes = SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE);
if((modes & SYMBOL_FILLING_FOK) != 0) return ORDER_FILLING_FOK;
if((modes & SYMBOL_FILLING_IOC) != 0) return ORDER_FILLING_IOC;
return ORDER_FILLING_RETURN;
}
3. Minimum stop distance
The tester usually reports SYMBOL_TRADE_STOPS_LEVEL as zero, so a 3-pip stop passes testing and gets rejected live with retcode 10016. A strategy can look respectable on paper — [a real backtest](https://mql.ranartech.com/r/IxZSfChQV6Fn) of a EURUSD H1 EA shows +$340 across 26 trades at a 73% win rate — and still fall over the moment its stops sit inside the broker's floor.
double MinStopDistance(const string symbol)
{
double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
long stops = SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL);
long freeze = SymbolInfoInteger(symbol, SYMBOL_TRADE_FREEZE_LEVEL);
return (double)MathMax(stops, freeze) * point;
}
Compare MathAbs(price - sl) against that value and widen the stop if it falls short. Also round SL and TP to the symbol's tick size, not to a hard-coded number of digits.
4. OrderSend is not instant
Under the tester, the deal is booked before OrderSend returns. Live, the server round-trip means PositionsTotal() may still be zero on the next line. Do not gate subsequent logic on a position you assume exists.
bool SendMarket(const string symbol, ENUM_ORDER_TYPE type, double lots,
double sl, double tp, ulong magic)
{
MqlTradeRequest req; MqlTradeResult res;
ZeroMemory(req); ZeroMemory(res);
req.action = TRADE_ACTION_DEAL;
req.symbol = symbol;
req.volume = lots;
req.type = type;
req.price = (type == ORDER_TYPE_BUY)
? SymbolInfoDouble(symbol, SYMBOL_ASK)
: SymbolInfoDouble(symbol, SYMBOL_BID);
req.sl = sl;
req.tp = tp;
req.deviation = 20;
req.magic = magic;
req.type_filling = PickFilling(symbol);
if(!OrderSend(req, res))
{ PrintFormat("OrderSend failed, retcode=%u", res.retcode); return false; }
if(res.retcode != TRADE_RETCODE_DONE && res.retcode != TRADE_RETCODE_PLACED)
{ PrintFormat("Rejected %u: %s", res.retcode, res.comment); return false; }
return true;
}
If you use OrderSendAsync, the only safe place to update state is OnTradeTransaction, on TRADE_TRANSACTION_DEAL_ADD.
5. Indicator handles aren't ready
In the tester, history is fully loaded before OnInit runs, so CopyBuffer returns data on the first tick. Live, the handle is still calculating and you get -1 with error 4806. Guard every read.
int h = INVALID_HANDLE;
int OnInit()
{
h = iMA(_Symbol, PERIOD_H1, 50, 0, MODE_EMA, PRICE_CLOSE);
if(h == INVALID_HANDLE)
{ Print("iMA failed: ", GetLastError()); return INIT_FAILED; }
return INIT_SUCCEEDED;
}
// inside OnTick
double buf[];
if(BarsCalculated(h) < 50) return;
if(CopyBuffer(h, 0, 0, 3, buf) < 3) return;
6. Silent ticks and new-bar detection
OnTick fires when a tick arrives, which on a quiet pair can be minutes apart. Anything time-based belongs in OnTimer. For new-bar logic, compare the last bar's open time rather than a tick counter — but seed it on the first tick, or the EA will fire immediately on attach.
datetime lastBar = 0;
bool IsNewBar(const string symbol, ENUM_TIMEFRAMES tf)
{
datetime t = (datetime)SeriesInfoInteger(symbol, tf, SERIES_LASTBAR_DATE);
if(t == 0) return false;
if(lastBar == 0) { lastBar = t; return false; }
if(t == lastBar) return false;
lastBar = t;
return true;
}
7. Netting accounts and position counting
OrdersTotal() counts pending orders, not positions — use PositionsTotal(). And on a netting account, a second buy merges into the existing position instead of opening a new one, so "one entry per bar" logic can double your size without ever showing two positions.
bool HasPosition(const string symbol, ulong magic)
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(PositionGetTicket(i) == 0) continue;
if(PositionGetString(POSITION_SYMBOL) == symbol &&
PositionGetInteger(POSITION_MAGIC) == magic)
return true;
}
return false;
}
Checklist
Before you blame the strategy, confirm: AutoTrading is on, the symbol is fully tradeable, the filling mode comes from the symbol, stops respect the broker minimum, every trade result is checked by retcode, indicator buffers are validated before use, IsNewBar is seeded, and position counts use PositionsTotal(). Almost every "works in tester, dead live" report is one of those seven.