expert-advisors debugging mql5 metatrader troubleshooting algo-trading

Common EA Failures & How to Debug Them

person
FXVPS Team
Share

Common EA Failures & How to Debug Them

Your EA worked perfectly in backtest. You deployed it on your VPS, went to bed confident, and woke up to discover it did absolutely nothing all night. No trades, no errors you can find, and your carefully tested strategy just sat there while the market moved without it.

Welcome to the real world of algorithmic trading, where the gap between “works in backtest” and “works in production” is filled with cryptic error codes, silent failures, and platform quirks that nobody documents properly.

We run thousands of MetaTrader instances across our VPS infrastructure. We have seen every failure mode there is. This guide is the distilled version of everything we have learned from supporting traders through the 3 AM “why is my EA not trading” panic messages. Bookmark it. You will need it.


1. Reading the Journal & Experts Tab

Before you can fix anything, you need to know where to look. MetaTrader gives you two separate log outputs, and most traders either ignore both or confuse the two.

The Experts tab shows output from your Expert Advisors specifically. Every Print() statement in your EA code appears here. More importantly, every runtime error your EA encounters — failed order sends, invalid parameters, access violations — gets logged here. This is your primary debugging tool.

The Journal tab shows terminal-level events: connections to the broker server, symbol synchronization, platform startup and shutdown, and account authentication. When your EA is not trading and the Experts tab is empty, the Journal is where you will find out that the terminal never connected to the broker in the first place.

Both tabs are accessible at the bottom of the MetaTrader terminal. Right-click inside either tab and select “Open” to find the actual log files on disk — they live in MQL5/Logs/ under the terminal’s data directory. For serious debugging, open these files in a text editor rather than scrolling through the tab. The terminal UI truncates old entries, but the files contain everything.

Log file locations by date:

%APPDATA%\MetaQuotes\Terminal\<instance-id>\MQL5\Logs\    (Experts logs)
%APPDATA%\MetaQuotes\Terminal\<instance-id>\logs\          (Journal logs)

On our VPS instances, you can also find these under C:\Users\<username>\AppData\Roaming\MetaQuotes\Terminal\. The instance ID is a long hex string — if you run multiple terminals, each gets its own.

One thing that trips people up: the Experts tab only shows logs for the currently selected chart. If your EA is attached to a different chart, switch to that chart first. The Journal tab is global.


2. Order Errors

These are the errors you will see most often, and each one has a specific cause and fix.

ERR_TRADE_DISABLED (4109 / 10017) — Trading is not allowed. This one has multiple causes: the “Allow Algo Trading” button in the toolbar is toggled off, the EA’s properties do not have “Allow Algo Trading” checked, or the broker has disabled trading on your account (common during account migration or compliance reviews). Check the AutoTrading button first — it is the most frequent cause and the most embarrassing.

ERR_TRADE_CONTEXT_BUSY (4108 / 10014) — Another operation is already using the trade context. We cover this in depth in Section 4, but the short version: only one EA can send a trade request at a time. If you have multiple EAs running, they will fight over the trade context.

ERR_NOT_ENOUGH_MONEY (10019) — Self-explanatory, but the cause is not always obvious. Your EA might be trying to open a position larger than your free margin allows, or you might have unrealized losses eating into your available margin. Always check AccountInfoDouble(ACCOUNT_MARGIN_FREE) before sending orders, and build in a buffer — do not use 100% of free margin.

ERR_INVALID_STOPS (10016) — Your stop loss or take profit is too close to the current price, or on the wrong side of it. Every symbol has a SYMBOL_TRADE_STOPS_LEVEL that defines the minimum distance (in points) that SL/TP must be from the current price. We cover this in Section 5.

ERR_MARKET_CLOSED (10018) — You are trying to trade outside of market hours. This is common with EAs that do not account for the gap between the forex market closing on Friday and opening on Sunday, or that attempt to trade instruments with limited sessions (commodities, indices) during off-hours.

The proper way to handle any order error in MQL5:

MqlTradeResult result;
MqlTradeRequest request;
// ... fill request ...

if(!OrderSend(request, result))
{
    Print("OrderSend failed: ", result.retcode, " - ", ResultRetcodeDescription(result.retcode));

    // Retryable errors
    if(result.retcode == TRADE_RETCODE_REQUOTE ||
       result.retcode == TRADE_RETCODE_PRICE_OFF ||
       result.retcode == TRADE_RETCODE_TOO_MANY_REQUESTS)
    {
        Sleep(1000);
        // retry logic here
    }
}

Never assume OrderSend succeeded without checking result.retcode. A return value of true from OrderSend only means the request was sent — you need result.retcode == TRADE_RETCODE_DONE to confirm execution.


3. Connection Issues

“No connection” is the most common reason an EA stops trading, and it is not always obvious because MetaTrader does not scream about it. The connection status indicator sits quietly in the bottom-right corner of the terminal, and if you are not watching it (or logging it), you will miss disconnections entirely.

Common causes of connection loss:

  • Broker server migration. Brokers periodically move accounts between servers. Your terminal is configured to connect to broker-live3.example.com, but your account now lives on broker-live5.example.com. The terminal will show “No connection” or “Invalid account.” Check your broker’s announcements or contact support.
  • Weekend disconnects that do not recover. MetaTrader disconnects over the weekend when markets close. Usually it reconnects automatically on Sunday evening. Sometimes it does not — especially if the broker restarts their servers during maintenance. A scheduled terminal restart on Sunday afternoon (before market open) prevents this entirely.
  • Firewall or network changes. On shared hosting environments (not ours, thankfully), network configurations can change. On a VPS, this is rare, but worth checking if the terminal suddenly cannot connect after previously working fine.

Detecting disconnections in code:

void OnTick()
{
    if(!TerminalInfoInteger(TERMINAL_CONNECTED))
    {
        Print("WARNING: Terminal not connected to broker server");
        return;  // Do not attempt any trade operations
    }

    // Additional check: is the account actually active?
    if(AccountInfoInteger(ACCOUNT_TRADE_MODE) == ACCOUNT_TRADE_MODE_DISABLED)
    {
        Print("WARNING: Trading disabled on account");
        return;
    }

    // Your trading logic here
}

The problem with OnTick() is that it only fires when a tick arrives — and if you are disconnected, you are not receiving ticks. For persistent connection monitoring, use OnTimer():

int OnInit()
{
    EventSetTimer(30);  // Check every 30 seconds
    return INIT_SUCCEEDED;
}

void OnTimer()
{
    if(!TerminalInfoInteger(TERMINAL_CONNECTED))
    {
        Print("Connection lost at ", TimeToString(TimeCurrent()));
        // Optionally: send a push notification
        SendNotification("EA disconnected from broker server");
    }
}

On our VPS plans, we run a watchdog that detects unresponsive terminals and restarts them automatically. But your EA should still handle disconnections gracefully on its own — defense in depth.


4. Trade Context Busy

This is the error that drives multi-EA setups insane. MetaTrader has a single trade context — a lock that ensures only one EA can send a trade request at a time. If EA #1 is in the middle of placing an order, EA #2’s OrderSend call will fail with ERR_TRADE_CONTEXT_BUSY.

Why it happens:

  • Multiple EAs running on the same terminal, all trying to trade on the same tick.
  • A single EA that calls OrderSend in a loop without checking whether the trade context is free.
  • Scripts running alongside EAs.

The sleep-and-retry pattern:

bool SendOrderWithRetry(MqlTradeRequest &request, MqlTradeResult &result, int maxRetries = 5)
{
    for(int attempt = 0; attempt < maxRetries; attempt++)
    {
        if(!MQLInfoInteger(MQL_TRADE_ALLOWED))
        {
            Print("Trading not allowed for this EA");
            return false;
        }

        // Check if trade context is free
        if(IsTradeContextBusy())
        {
            Print("Trade context busy, attempt ", attempt + 1, "/", maxRetries);
            Sleep(500 + MathRand() % 500);  // 500-1000ms random delay
            continue;
        }

        if(OrderSend(request, result))
        {
            if(result.retcode == TRADE_RETCODE_DONE || result.retcode == TRADE_RETCODE_PLACED)
                return true;
        }

        Print("OrderSend returned: ", result.retcode);
        Sleep(300);
    }

    Print("Failed to send order after ", maxRetries, " attempts");
    return false;
}

bool IsTradeContextBusy()
{
    return !MQLInfoInteger(MQL_TRADE_ALLOWED);
}

The random component in the sleep duration is critical. If two EAs both sleep for exactly 500ms, they will wake up at the same time and collide again. Adding randomness (a jitter) breaks the deadlock pattern.

A better architecture: If you are running 3+ EAs on one terminal, consider staggering their execution with OnTimer() instead of relying on OnTick(). Set each EA to a different timer interval (e.g., 5 seconds, 7 seconds, 11 seconds — use prime numbers to minimize collisions). This reduces contention dramatically compared to having all EAs fire on every tick.


5. Symbol & Specification Errors

This category causes more silent failures than any other, because many EAs hard-code symbol names and lot sizes that work on one broker but fail on another.

Wrong symbol name. Your EA references "EURUSD" but your broker uses "EURUSD.r" or "EURUSDm" or "EURUSD.ecn". The EA initializes without error, receives no ticks, and does nothing. Always use Symbol() for the current chart’s symbol, or validate custom symbol names at startup:

int OnInit()
{
    string sym = "EURUSD";
    if(!SymbolInfoInteger(sym, SYMBOL_EXIST))
    {
        // Try common suffixes
        string suffixes[] = {".r", "m", ".ecn", ".pro", "_SB", ".a"};
        bool found = false;
        for(int i = 0; i < ArraySize(suffixes); i++)
        {
            if(SymbolInfoInteger(sym + suffixes[i], SYMBOL_EXIST))
            {
                sym = sym + suffixes[i];
                found = true;
                break;
            }
        }
        if(!found)
        {
            Print("ERROR: Symbol ", sym, " not found on this broker");
            return INIT_FAILED;
        }
    }
    Print("Using symbol: ", sym);
    return INIT_SUCCEEDED;
}

Lot size constraints. Every symbol has minimum lot, maximum lot, and lot step values. Sending an order for 0.03 lots when the lot step is 0.1 will fail. Normalize your lot size:

double NormalizeLot(string symbol, double lots)
{
    double minLot  = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
    double maxLot  = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
    double lotStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);

    lots = MathMax(minLot, lots);
    lots = MathMin(maxLot, lots);
    lots = MathFloor(lots / lotStep) * lotStep;
    lots = NormalizeDouble(lots, (int)MathCeil(-MathLog10(lotStep)));

    return lots;
}

Minimum SL/TP distance. SYMBOL_TRADE_STOPS_LEVEL returns the minimum distance in points. If this value is 50 and you try to set a stop loss 30 points from the current price, the order will be rejected with ERR_INVALID_STOPS. Some brokers return 0 for this value, meaning no minimum, but many do not. Always check:

int stopsLevel = (int)SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL);
double point   = SymbolInfoDouble(symbol, SYMBOL_POINT);
double minDist = stopsLevel * point;

// Ensure your SL/TP is at least minDist away from price
if(MathAbs(price - slPrice) < minDist)
{
    Print("SL too close: need at least ", minDist, " distance");
}

Tick size mismatches. On some instruments (especially CFDs and futures), the tick size is not equal to the point size. Use SYMBOL_TRADE_TICK_SIZE for price normalization instead of assuming _Point is correct for rounding.


Time bugs are insidious because they often only manifest at specific times of the year or specific days of the week, making them extremely hard to catch in testing.

Server time vs local time. TimeCurrent() returns the broker’s server time. TimeLocal() returns the VPS or computer’s local time. These are almost never the same. If your EA uses time filters (“only trade between 08:00 and 12:00 London”), you need to know which timezone the broker server runs on and offset accordingly. Most brokers run on GMT+2 or GMT+3 (Eastern European Time), but some use GMT or New York time.

// Wrong: assumes server time is your local timezone
if(hour >= 8 && hour < 12) // Trade London session

// Right: calculate the offset
datetime serverTime = TimeCurrent();
datetime gmtTime    = serverTime - brokerGMTOffset * 3600;
MqlDateTime gmt;
TimeToStruct(gmtTime, gmt);
if(gmt.hour >= 8 && gmt.hour < 12) // London session in GMT

DST changes. Twice a year, broker server time shifts by one hour when daylight saving time changes in their timezone. If your EA hard-codes time offsets, it will trade the wrong hours for weeks before anyone notices. The fix is to either recalculate the offset dynamically or use a library that handles timezone conversions properly. At minimum, build an alert that fires when the detected offset changes.

Timer events not firing. OnTimer() only fires when the terminal is connected and the chart is active. If the terminal loses connection, timer events stop. They resume when the connection returns, but they do not “catch up” on missed intervals. Do not use OnTimer() for anything that assumes regular, uninterrupted execution.

Weekend gaps. TimeCurrent() stops updating when the market closes on Friday. It will still return Friday’s closing time on Saturday and Sunday. If your EA uses TimeCurrent() to calculate time elapsed, it will think no time has passed over the weekend. Use TimeLocal() or GetTickCount() for real-world elapsed time measurements.


7. Memory & Performance Issues

MetaTrader is a 32-bit application on MT4 and can run as 64-bit on MT5, but either way, memory discipline matters. We see terminals become unresponsive on VPS instances far more often than you would expect, and it is almost always a resource management problem.

Memory leaks from unclosed handles. Every FileOpen() needs a FileClose(). Every IndicatorCreate() needs an IndicatorRelease(). Every ChartIndicatorAdd() creates an indicator instance. If your EA creates indicator handles in OnTick() without releasing them, memory usage will climb steadily until the terminal crashes. Always create handles in OnInit() and release them in OnDeinit():

int maHandle = INVALID_HANDLE;

int OnInit()
{
    maHandle = iMA(Symbol(), PERIOD_CURRENT, 20, 0, MODE_SMA, PRICE_CLOSE);
    if(maHandle == INVALID_HANDLE)
    {
        Print("Failed to create MA indicator");
        return INIT_FAILED;
    }
    return INIT_SUCCEEDED;
}

void OnDeinit(const int reason)
{
    if(maHandle != INVALID_HANDLE)
        IndicatorRelease(maHandle);
}

Too many indicators. Each indicator handle consumes memory and CPU. We have seen traders load 15-20 indicators on a single chart, across 8 charts, then wonder why the terminal freezes. Every indicator recalculates on every tick. If your EA uses custom indicators, consider calculating the values internally rather than creating separate indicator handles — it is more work but dramatically more efficient.

Array management. Dynamic arrays that grow without bound are a classic memory leak. If you are appending trade history, price data, or signal records to an array in OnTick(), make sure you cap the array size or periodically flush old data. ArrayResize() with a reserve buffer reduces memory fragmentation:

// Bad: grows forever
ArrayResize(priceHistory, ArraySize(priceHistory) + 1);

// Better: cap at 10000 elements, shift old data out
if(ArraySize(priceHistory) >= 10000)
    ArrayRemove(priceHistory, 0, 1);  // Remove oldest element
ArrayResize(priceHistory, ArraySize(priceHistory) + 1);

The 2GB memory limit. MT4 is 32-bit and cannot use more than approximately 2GB of RAM regardless of how much your VPS has. MT5 in 64-bit mode does not have this limit in practice, but a single runaway EA can still consume enough memory to destabilize the terminal. Monitor memory usage in Task Manager — if your terminal is above 1GB and climbing, something is wrong.


8. The Nuclear Options

Sometimes debugging is not the answer. Sometimes you just need to blow it away and start fresh. Here is how, in escalating order of severity.

Restart the terminal. This fixes trade context locks that got stuck, timers that stopped firing, connections that will not re-establish, and minor memory leaks. It is the first thing to try and it fixes the problem about 60% of the time. On our VPS, right-click the terminal in the taskbar, close it, and relaunch from the desktop shortcut.

Delete the terminal data folder. This is for when the terminal itself is corrupted — crashing on startup, freezing immediately, or showing garbled data. But do not just nuke it blindly.

Export your settings first:

  1. Copy your MQL5/Experts/ folder (your EA files).
  2. Copy your MQL5/Presets/ folder (your saved EA parameter sets).
  3. Note which EAs are attached to which charts and with what parameters — right-click each EA, select “Properties,” and save the settings as a .set file via the “Save” button.
  4. Export your chart templates: right-click chart, Templates, Save Template.
  5. Copy any custom indicators from MQL5/Indicators/.

The data folder location is: File > Open Data Folder in MetaTrader. Once you have backed everything up, close the terminal, delete (or rename) the data folder, and restart. The terminal will create a fresh data folder. Reimport your EAs, indicators, presets, and reattach them to charts.

Reinstall MetaTrader. This is the last resort, and it is only necessary when the terminal binary itself is corrupted or when a platform update went wrong. Uninstall via Windows Add/Remove Programs, delete any remaining files in the installation directory (usually C:\Program Files\MetaTrader 5\), and install fresh from your broker’s download page. Then restore your backed-up files.

The critical step everyone forgets: After any nuclear option, do not just reattach your EAs and walk away. Watch the terminal for at least 15-30 minutes. Verify in the Experts tab that your EA is initialized, receiving ticks, and producing the log output you expect. Check that the AutoTrading button is enabled (it resets to disabled on fresh installs). Confirm your EA parameters loaded correctly from the preset file. We have seen traders go through the entire reinstall process, reattach their EA with default parameters instead of their tuned ones, and not notice for days.


A Debugging Mindset

The traders who maintain consistently running EAs are not the ones who never encounter errors. They are the ones who built logging into their EAs from day one, who check their Experts tab every morning, and who treat every unexplained behavior as a bug worth investigating rather than a fluke to ignore.

Add Print() statements generously during development. Log every order attempt, every parameter calculation, every time filter decision. Yes, the log files will be large. Storage is cheap. Missed trades and silent failures are expensive.

And when you do hit a problem you cannot solve, check the logs first. Then check the Journal. Then check Task Manager. Then check the connection status. Ninety percent of the time, the answer is in one of those four places. The other ten percent is why guides like this exist.