MT5 EA Not Opening Trades: MQL5 Debugging Checklist

MT5 EA Not Opening Trades: MQL5 Debugging Checklist

By HorizonAI Team · 15 min read · Intermediate

MT5 EA Not Opening Trades? Debug Your MQL5 Entry Path

An EA can compile, attach to a chart, and still never send a single order. The fastest fix is to stop treating every silent EA as the same failure. First determine whether it never saw a valid signal, saw one but blocked itself before sending, or sent a request that the terminal or trade server rejected.

Short answer: Check the Experts and Journal tabs first, then log the decision at each new bar: permissions, symbol prices, spread, volume, signal state, margin, and the trade result code. In MQL5, a successful CTrade.Buy() or Sell() method call does not by itself prove that a deal was executed. Read the server retcode and its description after each request.

The checklist below gives you a controlled path: prove that OnTick() runs, prove that your crossover occurs, prove that the order is permitted, then prove the server accepted it. The instrumented EMA EA later in the guide prints each decision so you don't have to guess. If you're still building the strategy logic, see this guide to coding a Supertrend indicator and EA in MQL5 for a second EA structure to compare.

Diagnose the failure before changing the strategy

Start with this three-way split. It prevents the common mistake of changing entry rules when the EA is actually blocked by a setting or invalid volume.

What you observeMost likely meaningFirst proof to collect
No log line appears when price movesEA isn't receiving ticks or OnTick() exits earlyAdd PrintFormat() at the start of OnTick() and inspect Experts
Logs show no crossover or setupYour signal condition wasn't true on closed barsPrint current and prior indicator values
Logs show a signal but no orderA local gate blocked itLog AutoTrading, spread, volume, margin, position filter
An order method ran but no position appearsServer rejected the request, or you are looking at the wrong placePrint ResultRetcode() and ResultRetcodeDescription()
Position exists in Tester but not on the chartTester and chart are separate environmentsInspect the Tester Results/Graph and its Journal

The Experts tab is where an EA's Print and PrintFormat output lands. The Journal tab adds terminal-level messages, including trading permissions and connection events. MQL5 calls OnTick() only when a new tick arrives for the chart symbol, so a quiet market or a symbol without incoming quotes can look exactly like a broken entry condition. The platform's event-handler reference documents that one EA runs per chart and tick events are not queued indefinitely if processing is already underway. MQL5's OnTick reference is worth keeping open while debugging event flow.

Don't start by adding more indicators. Put one diagnostic line at every return path. A log that says BLOCK: spread 28.0 > 20 is a fix. A chart with no arrows is only a symptom.

Fix 1: confirm AutoTrading and EA permissions

An EA can run its calculations while being forbidden to trade. That distinction catches many cases where the smiley or EA name on a chart makes the setup look complete.

Check these layers in order:

  1. Turn on the terminal's Algo Trading/AutoTrading control.
  2. Open the EA's properties on the chart and enable Allow Algo Trading.
  3. Make sure the account permits Expert Advisor trading. Investor or read-only logins cannot send orders.
  4. In Strategy Tester, enable the relevant trading permissions and test the same EA inputs you use on the chart.
  5. Confirm the EA has not coded its own permission gate, such as a terminal or account check that returns false.

The useful programmatic checks are MQLInfoInteger(MQL_TRADE_ALLOWED), TerminalInfoInteger(TERMINAL_TRADE_ALLOWED), and AccountInfoInteger(ACCOUNT_TRADE_EXPERT). Log all three values instead of assuming the chart button tells the whole story.

Mistake: Seeing the EA's initialization message and assuming it can place orders.

Do this: Print each permission flag on the first new bar. If one is zero, fix the platform or EA property setting before inspecting any signal logic.

Fix 2: prove OnTick() and new-bar gating are working

Many EAs deliberately evaluate only once per bar. That's sensible for a moving-average crossover, but the gate can hide the real issue. If the EA initializes after the current bar starts, a typical new-bar function will return on its first call, then wait until the next candle opens.

Test the event path in this order:

  • Attach the EA to a symbol that is receiving live quotes, or run the Strategy Tester with a date range containing data.
  • Print the symbol, timeframe, and TimeCurrent() at the top of OnTick() during a short test.
  • Print when the new-bar gate rejects a tick and when it accepts a bar.
  • Use closed-bar values for the signal. For an EMA crossover, compare bar 1 against bar 2, not the still-forming bar 0.

A crossover requires a change of state. A valid long example is fast[2] <= slow[2] and fast[1] > slow[1]. If you only check fast > slow, the condition can stay true for 30 bars and your one-position gate may correctly block every later bar.

This is close to the discipline needed when testing any systematic setup. Your backtesting metrics only mean something after the strategy's event timing and entries are correctly defined.

Fix 3: inspect the exact symbol, session, and quotes

EURUSD in a code example may be EURUSD.a, EURUSDm, or another broker-specific symbol in your terminal. An EA attached to the correct-looking chart normally receives that chart's _Symbol, but hard-coded symbols, multi-symbol EAs, and SymbolSelect() failures can leave you querying a name that has no usable quote.

Print _Symbol, SymbolInfoTick() values, and the symbol's trade mode. Then look at these conditions:

  • Bid and ask are nonzero. Zero prices indicate no current quote.
  • The symbol is selected and visible. Call SymbolSelect(symbol, true) for symbols used outside the attached chart.
  • Trade mode allows the direction. A symbol may be disabled, close-only, long-only, or short-only.
  • The market is open for that instrument. A forex session, exchange future, CFD, and crypto symbol do not share one universal schedule.
  • The spread filter uses points, not a guessed pip conversion. On a 5-digit forex quote, 10 points equals one pip.

MQL5 exposes symbol constraints through SymbolInfoInteger and SymbolInfoDouble, including trade mode, minimum volume, stop level, and freeze level. The symbol and market-information constants are the authoritative map of those fields.

Mistake: Hard-coding "EURUSD" because it worked in a demo terminal.

Do this: Default to _Symbol. For a multi-symbol EA, expose the symbol as an input, call SymbolSelect, and log the actual name and bid/ask before calculating a signal.

Fix 4: normalize lots and respect stop distances

A request can be logically correct and still be invalid for the broker's contract specification. The two frequent culprits are an unsupported volume and stops placed too close to the market.

Every tradable symbol has a SYMBOL_VOLUME_MIN, SYMBOL_VOLUME_MAX, and SYMBOL_VOLUME_STEP. If the minimum is 0.10 and your input is 0.01, the EA must not send 0.01 and hope the server rounds it. Normalize it deliberately, log the result, and decide whether rounding down changes the intended risk too much.

Stop loss and take profit distances are in points in the EA below. They must be at least the symbol's SYMBOL_TRADE_STOPS_LEVEL; using the larger of stop and freeze distances provides a conservative guard. A market order without SL/TP can still be accepted where an otherwise identical order with too-close protection is rejected.

Use OrderCheck() when you need a preflight view of a manually populated MqlTradeRequest. It estimates whether the request can be accepted and reports projected margin and return information, but it is not a promise of execution because market conditions can change before the request reaches the server. See the MQL5 OrderCheck documentation for the request fields and result structure.

Fix 5: remove position and magic-number blind spots

A one-position rule is good risk control only if it checks the positions you actually mean to block. On netting accounts, one position exists per symbol, while hedging accounts can hold multiple positions. A filter that only calls PositionSelect(_Symbol) can block a new trade because of a manual position or another EA's position.

For a strategy-specific limit, scan positions and compare both POSITION_SYMBOL and POSITION_MAGIC. Set the magic number once in OnInit() with trade.SetExpertMagicNumber(). If your tester shows trades but your log says position already open, print the ticket, magic number, and position type so you know exactly what created the block.

Also check whether the direction test is too broad. If the EA allows one buy but should reverse into a sell, decide explicitly whether it closes the buy first, skips the sell, or permits a hedge. Silence caused by an unstated position policy is still a logic bug.

A diagnostic EMA-crossover EA that explains every skip

The EA below uses a 9/21 EMA crossover on completed bars, a 0.10-lot default, a 20-point maximum spread, 300-point stop loss, 600-point take profit, magic number 260820, and one-position toggle. It prints why it skipped a bar and reports the trade-server result after a buy or sell attempt.

#property strict
#include <Trade/Trade.mqh>

input int    InpFastEMA          = 9;
input int    InpSlowEMA          = 21;
input double InpLots             = 0.10;
input int    InpMaxSpreadPoints  = 20;
input int    InpStopLossPoints   = 300;
input int    InpTakeProfitPoints = 600;
input long   InpMagicNumber      = 260820;
input bool   InpOnePositionOnly  = true;

CTrade trade;
int fastHandle = INVALID_HANDLE;
int slowHandle = INVALID_HANDLE;
datetime lastBarTime = 0;

int VolumeDigits(const double step)
{
   double workingStep = step;
   int digits = 0;
   while(workingStep < 1.0 && digits < 8)
   {
      workingStep *= 10.0;
      digits++;
   }
   return digits;
}

double NormalizeVolume(const double requestedVolume)
{
   const double minVolume = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   const double maxVolume = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   const double volumeStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);

   if(minVolume <= 0.0 || maxVolume <= 0.0 || volumeStep <= 0.0)
   {
      PrintFormat("BLOCK: invalid volume specification min=%.4f max=%.4f step=%.4f",
                  minVolume, maxVolume, volumeStep);
      return 0.0;
   }

   double normalized = MathFloor(requestedVolume / volumeStep + 1e-8) * volumeStep;
   normalized = MathMax(minVolume, MathMin(maxVolume, normalized));
   return NormalizeDouble(normalized, VolumeDigits(volumeStep));
}

bool IsNewBar()
{
   const datetime currentBarTime = iTime(_Symbol, _Period, 0);
   if(currentBarTime == 0)
      return false;

   if(currentBarTime == lastBarTime)
      return false;

   lastBarTime = currentBarTime;
   return true;
}

bool HasStrategyPosition()
{
   for(int index = PositionsTotal() - 1; index >= 0; index--)
   {
      const ulong ticket = PositionGetTicket(index);
      if(ticket == 0)
         continue;

      const string positionSymbol = PositionGetString(POSITION_SYMBOL);
      const long positionMagic = PositionGetInteger(POSITION_MAGIC);
      if(positionSymbol == _Symbol && positionMagic == InpMagicNumber)
         return true;
   }
   return false;
}

bool CanTradeNow(const double volume, string &reason)
{
   if(!MQLInfoInteger(MQL_TRADE_ALLOWED))
   {
      reason = "EA properties do not allow algo trading";
      return false;
   }
   if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
   {
      reason = "terminal Algo Trading is disabled";
      return false;
   }
   if(!AccountInfoInteger(ACCOUNT_TRADE_EXPERT))
   {
      reason = "account does not allow Expert Advisor trading";
      return false;
   }

   MqlTick tick;
   if(!SymbolInfoTick(_Symbol, tick) || tick.bid <= 0.0 || tick.ask <= 0.0)
   {
      reason = "no valid bid/ask quote";
      return false;
   }

   const long tradeMode = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_MODE);
   if(tradeMode == SYMBOL_TRADE_MODE_DISABLED || tradeMode == SYMBOL_TRADE_MODE_CLOSEONLY)
   {
      reason = "symbol is disabled or close-only";
      return false;
   }

   const double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   const double spreadPoints = (tick.ask - tick.bid) / point;
   if(spreadPoints > InpMaxSpreadPoints)
   {
      reason = StringFormat("spread %.1f exceeds limit %d", spreadPoints, InpMaxSpreadPoints);
      return false;
   }

   double requiredMargin = 0.0;
   if(!OrderCalcMargin(ORDER_TYPE_BUY, _Symbol, volume, tick.ask, requiredMargin))
   {
      reason = StringFormat("OrderCalcMargin failed, error %d", GetLastError());
      return false;
   }
   if(AccountInfoDouble(ACCOUNT_MARGIN_FREE) < requiredMargin)
   {
      reason = StringFormat("free margin %.2f is below required %.2f",
                            AccountInfoDouble(ACCOUNT_MARGIN_FREE), requiredMargin);
      return false;
   }

   reason = "ok";
   return true;
}

bool SendMarketOrder(const ENUM_ORDER_TYPE orderType, const double volume)
{
   MqlTick tick;
   if(!SymbolInfoTick(_Symbol, tick))
   {
      Print("ORDER BLOCK: no current tick");
      return false;
   }

   const double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   const int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   const long stopsLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   const long freezeLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL);
   const double minimumDistance = MathMax((double)stopsLevel, (double)freezeLevel) * point;
   const double stopDistance = MathMax((double)InpStopLossPoints * point, minimumDistance);
   const double targetDistance = MathMax((double)InpTakeProfitPoints * point, minimumDistance);

   bool sent = false;
   if(orderType == ORDER_TYPE_BUY)
   {
      const double stopLoss = NormalizeDouble(tick.ask - stopDistance, digits);
      const double takeProfit = NormalizeDouble(tick.ask + targetDistance, digits);
      sent = trade.Buy(volume, _Symbol, 0.0, stopLoss, takeProfit, "Diagnostic EMA buy");
   }
   else
   {
      const double stopLoss = NormalizeDouble(tick.bid + stopDistance, digits);
      const double takeProfit = NormalizeDouble(tick.bid - targetDistance, digits);
      sent = trade.Sell(volume, _Symbol, 0.0, stopLoss, takeProfit, "Diagnostic EMA sell");
   }

   PrintFormat("ORDER %s: method=%s retcode=%u description=%s deal=%I64u order=%I64u",
               orderType == ORDER_TYPE_BUY ? "BUY" : "SELL",
               sent ? "true" : "false",
               trade.ResultRetcode(),
               trade.ResultRetcodeDescription(),
               trade.ResultDeal(),
               trade.ResultOrder());
   return sent;
}

int OnInit()
{
   if(InpFastEMA >= InpSlowEMA || InpFastEMA < 1)
   {
      Print("INIT FAILED: fast EMA must be positive and lower than slow EMA");
      return INIT_PARAMETERS_INCORRECT;
   }

   SymbolSelect(_Symbol, true);
   trade.SetExpertMagicNumber(InpMagicNumber);
   trade.SetDeviationInPoints(10);

   fastHandle = iMA(_Symbol, _Period, InpFastEMA, 0, MODE_EMA, PRICE_CLOSE);
   slowHandle = iMA(_Symbol, _Period, InpSlowEMA, 0, MODE_EMA, PRICE_CLOSE);
   if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE)
   {
      PrintFormat("INIT FAILED: iMA handle error %d", GetLastError());
      return INIT_FAILED;
   }

   PrintFormat("INIT: symbol=%s period=%s algo=%d terminal=%d account=%d",
               _Symbol,
               EnumToString(_Period),
               MQLInfoInteger(MQL_TRADE_ALLOWED),
               TerminalInfoInteger(TERMINAL_TRADE_ALLOWED),
               AccountInfoInteger(ACCOUNT_TRADE_EXPERT));
   return INIT_SUCCEEDED;
}

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

void OnTick()
{
   if(!IsNewBar())
      return;

   double fastClosed[1];
   double fastPrior[1];
   double slowClosed[1];
   double slowPrior[1];
   if(CopyBuffer(fastHandle, 0, 1, 1, fastClosed) != 1 ||
      CopyBuffer(fastHandle, 0, 2, 1, fastPrior) != 1 ||
      CopyBuffer(slowHandle, 0, 1, 1, slowClosed) != 1 ||
      CopyBuffer(slowHandle, 0, 2, 1, slowPrior) != 1)
   {
      PrintFormat("BLOCK: CopyBuffer failed, error %d", GetLastError());
      return;
   }

   const bool buySignal = fastPrior[0] <= slowPrior[0] && fastClosed[0] > slowClosed[0];
   const bool sellSignal = fastPrior[0] >= slowPrior[0] && fastClosed[0] < slowClosed[0];
   PrintFormat("BAR: fast[2]=%.5f slow[2]=%.5f fast[1]=%.5f slow[1]=%.5f buy=%s sell=%s",
               fastPrior[0], slowPrior[0], fastClosed[0], slowClosed[0],
               buySignal ? "true" : "false", sellSignal ? "true" : "false");

   if(!buySignal && !sellSignal)
      return;

   if(InpOnePositionOnly && HasStrategyPosition())
   {
      Print("BLOCK: an open position with this symbol and magic number already exists");
      return;
   }

   const double volume = NormalizeVolume(InpLots);
   if(volume <= 0.0)
      return;

   string reason;
   if(!CanTradeNow(volume, reason))
   {
      PrintFormat("BLOCK: %s", reason);
      return;
   }

   const long tradeMode = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_MODE);
   if(buySignal && tradeMode == SYMBOL_TRADE_MODE_SHORTONLY)
   {
      Print("BLOCK: symbol permits short positions only");
      return;
   }
   if(sellSignal && tradeMode == SYMBOL_TRADE_MODE_LONGONLY)
   {
      Print("BLOCK: symbol permits long positions only");
      return;
   }

   SendMarketOrder(buySignal ? ORDER_TYPE_BUY : ORDER_TYPE_SELL, volume);
}

CTrade is useful here because it packages common trade operations and exposes the result methods used in the log. The MQL5 CTrade class reference lists those methods. Notice the design choice: SendMarketOrder() reports both the Boolean returned by Buy or Sell and the server's retcode. Treat the retcode as the verdict.

How to read the diagnostic output

Run this in Strategy Tester first. Set a visible date range, choose the same symbol and timeframe you plan to use, and open the Tester Journal after the run. Then attach it to a chart only after you can explain each log line.

  • BAR ... buy=false sell=false means the EA is healthy but no completed-bar crossover occurred.
  • BLOCK: spread ... means relax InpMaxSpreadPoints only after confirming the symbol's point size. A 20-point ceiling is two pips on many 5-digit forex symbols, but not on every CFD or index.
  • BLOCK: free margin ... means reduce volume, add funds to the test account, or inspect the contract's margin settings.
  • ORDER BUY: method=false retcode=... means inspect the description first, then verify volume, stops, trade mode, and market session.
  • ORDER BUY: method=true still requires the retcode description. A method call can pass local validation while the trade server returns a result you need to handle.

Tester settings and chart behavior are not interchangeable

The Strategy Tester uses its selected symbol, model, deposit, leverage, execution assumptions, and historical date range. A chart EA uses the broker account's current symbol specification, live spread, session, and permissions. A strategy that enters in the Tester can legitimately skip on a live chart because spread is wider, the market is closed, or the position filter finds an existing trade.

Test one variable at a time. First set the maximum spread high enough to prove signals occur. Next restore the intended filter and observe how many signals it blocks. Then test the desired lot size against current margin. This staged approach is more reliable than changing five inputs after a quiet day.

If the order shows in Tester but not in your account history, you are likely looking at two different environments, not a missing trade. The Tester never places a real account order.

Common MQL5 EA entry mistakes

Mistake: Checking the crossover on bar 0, then wondering why an entry appeared and disappeared while the candle was open.

Do this: Compare bars 1 and 2, as the EA does. It acts only after the signal candle has closed.

Mistake: Using a fixed 0.01 lot size across forex, metals, indices, and CFDs.

Do this: Read SYMBOL_VOLUME_MIN and SYMBOL_VOLUME_STEP, normalize the requested volume, and print both values before sending an order.

Mistake: Logging only trade.Buy() returning false.

Do this: Log ResultRetcode() and ResultRetcodeDescription() every time. The description tells you whether to inspect permissions, price, stops, margin, or market state.

Mistake: Calling PositionSelect(_Symbol) and assuming every position belongs to this EA.

Do this: Filter by symbol and magic number. On a hedging account, scan all positions if your rule is one position per strategy.

Mistake: Testing a broker-suffixed symbol on the chart but calculating a hard-coded unsuffixed symbol in code.

Do this: Use _Symbol for a single-chart EA. Expose an input symbol and call SymbolSelect() only when you intentionally build a multi-symbol system.

Pro tips for faster order-path debugging

Make each rejection searchable. Prefix every skipped setup with BLOCK: and every attempted order with ORDER:. After a 12-month test, filter the Journal for those terms instead of scrolling through indicator messages.

Log the signal only once per bar. A 9/21 crossover should be evaluated on bar close for this design. The IsNewBar() gate keeps one tick burst from generating repeated requests and makes logs readable.

Temporarily separate signal proof from trade proof. During diagnosis, print the crossover and set InpOnePositionOnly=false in a controlled tester run. If signals print but no orders follow, the fault sits in the permission, constraint, or request path, not the EMA math.

Use a fixed magic number per strategy version. Change it when you materially alter entry or exit rules. That makes it obvious whether an open position came from the current EA or an older build.

For a broader process around test assumptions, commission, and reviewing results, pair this checklist with how to backtest a trading strategy. The order path must be correct before any performance metric is worth optimizing.

Generating this diagnostic EA without writing the code yourself

You can have HorizonAI generate or revise this type of MQL5 Expert Advisor from plain-English chat, then edit the result in its browser editor. It generates MQL5 code and compile-checks it, but it doesn't place trades or connect to your broker.

Paste a prompt like this:

Build an MQL5 Expert Advisor for the chart symbol and timeframe. Enter long when the 9 EMA crosses above the 21 EMA on completed bars, and short on the opposite cross. Use inputs for 0.10 lots, maximum spread of 20 points, 300-point stop loss, 600-point take profit, magic number 260820, and a one-position-per-symbol-and-magic toggle. Add PrintFormat diagnostics for OnInit permissions, each completed-bar EMA value, every skipped trade with a BLOCK prefix, normalized volume, margin checks, and CTrade ResultRetcode plus ResultRetcodeDescription after Buy or Sell. Use closed bars only and make the full EA compile in MQL5.

Or, if you already have an EA that stays silent:

Debug this MQL5 EA. Add logging that separates no signal, local trade block, and server-rejected order. Preserve my entry rules, normalize volume from SYMBOL_VOLUME_MIN/MAX/STEP, validate spread and margin, and print CTrade retcodes after every order attempt.

You'll get editable MQL5 code you can refine in chat, including compiler feedback when a change breaks the script. Try it free →

FAQs

Why does my MT5 EA show a smiley face but not open trades?

The EA is attached and initialized, but that doesn't prove it has a signal or permission to trade. Check the Experts log for the current signal state, AutoTrading flags, spread, volume, and server retcode after an attempt.

Why does CTrade.Buy() return true but no deal appears?

A true return indicates the request structure passed the method's basic checks. Read ResultRetcode() and ResultRetcodeDescription() to learn the trade server's actual response, then inspect the Tester Journal or account history in the correct environment.

How do I know whether my MQL5 EA has no signal or cannot trade?

Print the completed-bar indicator values and Boolean signal first. If a buy or sell signal is true, log each subsequent gate, including permissions, quote, spread, volume, margin, position filter, and order result.

Does the MT5 Strategy Tester use the same conditions as a live chart?

No. The Tester uses its own selected historical period and test settings, while a chart uses current broker quotes, session conditions, account permissions, and contract specifications. Match the symbol and inputs, then compare logs rather than assuming the two environments behave identically.

Final thoughts

A silent EA isn't a mystery once its order path is visible. Start with the Experts log, classify the failure as no signal, local block, or rejected request, and make every return path state its reason.

The practical habit to keep: never change an EMA period, lot size, and spread filter in the same test. Prove one layer, then move to the next.

Related articles

Questions about MQL5 EA debugging? Join our Discord to discuss with other traders!